feat: update web Interface

This commit is contained in:
Yao Wang 2022-07-29 14:22:57 +02:00
parent d5d633b6c7
commit f9844dba10
1111 changed files with 171093 additions and 0 deletions

View file

@ -0,0 +1,624 @@
/* UI Parameters *******************************************************************/
const PARAMS = {
ENABLE_MSEC_SET_URL: false, // enables setting image display time in the url like so: '?msecImg=1000'
NUM_MSEC_CROSS: 750, // how long the fixation cross is shown for
NUM_MSEC_IMAGE: 20000, // num milliseconds to show each image for
NUM_MSEC_SENTINEL: 750, // how long a sentinel image is shown for
NUM_MSEC_CHAR: 400, // how long the code chart is shown
IMG_WIDTH: 1600, // max img width
IMG_HEIGHT: 800, // max img height
N_BUCKETS: 1,
N_SUBJ_FILES: 1,
MAX_INVALID_ALLOWED_TUTORIAL: 1,
MAX_INCORRECT_SENTINELS_ALLOWED_TUTORIAL: 0,
GIVE_FEEDBACK: true, // should feedback be given during the tutorial
NUM_MSEC_FEEDBACK: 2000, // how long does the feedback stay on the screen
}
// messages shown if feedback is given
const POSITIVE_MESSAGE = "Keep up the good work!";
const NEGATIVE_MESSAGE = "Please type the triplet you see when the image vanishes.";
// path to the task data to use
const DATA_BASE_PATH = "assets/task_data/";
// base path relative to which image paths are defined
const IMAGE_BASE_PATH = "assets/"
const SUBJECT_FILES_BASE_PATH = DATA_BASE_PATH + "full_subject_files/";
const FIXATION_CROSS = DATA_BASE_PATH + "fixation-cross.jpg"
const TARGET_NUM = 40
/* Global vars **********************************************************************/
// variables we want to save to store to outputs
var SUBJ_ID;
var BUCKET_NUM;
var SUBJ_FILE_PATH;
var OPEN_TIME; // when the url is first loaded
var START_TIME; // when they first click the "continue" button
var LOAD_TIME; // when the last image loads and the task is ready to go
// normal images
var IMAGES = new Array(); // preload the images
var CHECKED_TUTORIAL_FLAG = false;
var IS_TUTORIAL = true;
var MESSAGE = "";
var MESSAGE_IS_POSITIVE = true;
// during the task, keeps track of how the participant is doing
// this count is only valid up until you hit the submit button
var SCORES = {
SENTINEL_CORRECT: 0,
SENTINEL_TOTAL: 0,
IMAGE_CORRECT: 0,
IMAGE_TOTAL: 0,
INVALID: 0
}
// The answers picked by user
var ANSWERS = []
var ANSWER_TMP = []
var RECO_ANSWERS = []
/* End vars ************************************************************/
var custom = {
loadTasks: function() {
/*
* Loads data needed to run the task and does some one-time setup, such as:
* - timestamping the start of the task
* - selecting a subject file to use and loading it
* - preloading images
*
* returns [obj, int]: A length-two list where the first element is the loaded task data
* and the second element is the number of trails (number of images) in the task.
*/
OPEN_TIME = new Date();
DEBUG = gup("debug") == "true";
$(".instruction-button").click(function() {
START_TIME = new Date();
$(this).unbind();
})
//hide all subtasks to begin with
$(".subtask").hide();
// set the size of the images
$("img.img-main").css({
"width": "100%",
"height": "100%",
"objectFit": "contain"
});
$("img.img-blur").css({
"width": "100%",
"height": "100%",
"objectFit": "contain"
});
BUCKET_NUM = gupOrRandom("bucket", PARAMS.N_BUCKETS);
SUBJ_ID = gupOrRandom("subj", PARAMS.N_SUBJ_FILES);
console.log("Bucket", BUCKET_NUM, "subjId", SUBJ_ID);
SUBJ_FILE_PATH = SUBJECT_FILES_BASE_PATH + "bucket" + BUCKET_NUM + "/subject_file_" + SUBJ_ID + ".json";
return $.get(SUBJ_FILE_PATH).then(function(tasks) {
console.log(tasks)
/*if (DEBUG) {
if (tasks.length > 1) {
tasks = tasks.slice(0, 1);
}
}*/
// pre-load all the images
preloadImages(tasks);
// set the correct image exposure
if (PARAMS.ENABLE_MSEC_SET_URL) {
var urlMsecImg = gup('msecImg');
if (urlMsecImg.length > 0) {
PARAMS.NUM_MSEC_IMAGE = parseInt(urlMsecImg);
}
}
console.log("Trial start:", new Date());
console.log("Image exposure time:", PARAMS.NUM_MSEC_IMAGE);
console.log("CC exposure time:", PARAMS.NUM_MSEC_CHAR);
console.log("S exposure time:", PARAMS.NUM_MSEC_SENTINEL);
return [tasks, tasks.length];
});
},
showTask: function(taskInput, taskIndex, taskOutput) {
/*
* Shows the next trial of the experiment (fix cross, image, code chart, and character input.)
*
* taskInput - the task data returned from loadTasks
* taskIndex - the number of the current subtask (image)
* taskOutput - a partially filled out object containing the results (so far) of the task.
*
* returns: None
*/
var nMsecFeedback = (PARAMS.GIVE_FEEDBACK && MESSAGE && IS_TUTORIAL) ? PARAMS.NUM_MSEC_FEEDBACK : 0;
var messageClass = MESSAGE_IS_POSITIVE ? "positive" : "negative";
// terminate task early if they do not have required performance on tutorial
IS_TUTORIAL = isTutorial(taskInput[taskIndex]);
if (IS_TUTORIAL || didEndTutorial(taskInput, taskIndex, taskOutput)) {
if (!passedTutorial()) {
return;
}
}
$(".subtask").hide();
$('#next-button').hide(); // Hide the next button; we will handle control flow for this task
$('#qa-button').hide();
$('#nq-button').hide();
$("#reco-button").hide();
hideIfNotAccepted();
if (nMsecFeedback > 0) {
var feedbackMessageElt = $("#feedback-message");
feedbackMessageElt.empty();
feedbackMessageElt.append('<div class="ui message ' + messageClass + '"><div class="header">' + MESSAGE + '</div></div>');
$("#feedback-subtask").show();
}
setTimeout(showFixationCross.bind(this, taskInput, taskIndex, taskOutput), nMsecFeedback);
},
collectData: function(taskInput, taskIndex, taskOutput) {
/*
* Records the experimental output for the current subtask (image).
*
* taskInput - the task data returned from loadTasks
* taskIndex - the number of the current subtask (image)
* taskOutput - a partially filled out object containing the results (so far) of the task.
*
* returns: the new taskOutput object containing the data for the current subtask.
*/
var rememberedCode = $("#remembered-char").val().toUpperCase();
var isValidCode = _includes(taskInput[taskIndex].codes, rememberedCode);
//var isValidCode = _isCodePresent(rememberedCode, taskInput[taskIndex].codes);
var coord = isValidCode ? taskInput[taskIndex].coordinates[rememberedCode] : false;
taskOutput[taskIndex] = {
rememberedCode: rememberedCode,
isValidCode: isValidCode,
coordinate: coord
};
return taskOutput;
},
validateTask: function(taskInput, taskIndex, taskOutput) {
/*
* Reports whether the data corresponding to the current
* subtask (image) is valid (e.g. fully filled out)
*
* taskInput - the task data returned from loadTasks
* taskIndex - the number of the current subtask (image)
* taskOutput - a partially filled out object containing the results (so far) of the task.
*
* returns: falsey value if validation passed for the taskIndex-th subjtask.
* Truthy value if validation failed. To display a specific error message,
* return an object of the form {errorMessage: ""}
*/
// Answer rejection
/*if(ANSWERS.length>=5){
for(var i = 0; i < ANSWERS.length-5; i++) {
if(ANSWERS[i]==ANSWERS[i+1]&ANSWERS[i]==ANSWERS[i+3]&ANSWERS[i]==ANSWERS[i+2]&ANSWERS[i]==ANSWERS[i+4]){
return {errorMessage: "Sorry, you are not allowed to continue."};
}
}
}*/
// keep track of scores
var validCode = taskOutput[taskIndex].isValidCode;
SCORES.INVALID += !validCode;
var correctTrial;
if (isSentinel(taskInput[taskIndex])) {
SCORES.SENTINEL_TOTAL += 1;
var codeEntered = taskOutput[taskIndex].rememberedCode;
var correctCodes = taskInput[taskIndex].correct_codes;
if (!correctCodes) throw new Error("Correct codes were not provided in the subject file!");
var gotSentinel = _includes(correctCodes, codeEntered);
SCORES.SENTINEL_CORRECT += gotSentinel;
correctTrial = gotSentinel;
} else {
SCORES.IMAGE_TOTAL += 1;
SCORES.IMAGE_CORRECT += validCode;
correctTrial = validCode;
}
MESSAGE = correctTrial ? POSITIVE_MESSAGE : NEGATIVE_MESSAGE;
MESSAGE_IS_POSITIVE = correctTrial;
console.log('Invalid scores answered so far:', SCORES.INVALID);
return false; // we'll allow the task to continue either way but we'll remember if an invalid code was entered
},
getUploadPayload: function(taskOutputs) {
/*
* Returns the final output object to be saved from the task.
*
* taskInput - the task data returned from loadTasks
* taskOutput - a fully filled out object containing the results of the task.
*
* returns: all the data you want to be stored from the task.
*/
var endTime = new Date();
// compile timing data
var timing = {
openTime: OPEN_TIME,
loadTime: LOAD_TIME,
startTime: START_TIME,
endTime: endTime,
timeToCompleteFromOpenMsec: endTime - OPEN_TIME,
timeToLoadMsec: LOAD_TIME - OPEN_TIME,
timeToCompleteFromStartMsec: endTime - START_TIME
}
// put the survey data under a separate key
var surveyData = taskOutputs.survey_data;
delete taskOutputs.survey_data;
// task outputs to store, including timing data
var new_taskoutputs = {};
for (var i = 0; i < TARGET_NUM; i++) {
new_taskoutputs[i] = taskOutputs[i];
}
var outputs = {
timing: timing,
surveyData: surveyData,
tasks: new_taskoutputs,
qa_answers: ANSWERS,
re_answers: RECO_ANSWERS
}
return {
'outputs': outputs
}
},
getPayload: function(taskInputs, taskOutputs) {
/*
* Returns the final output object to be saved from the task.
*
* taskInput - the task data returned from loadTasks
* taskOutput - a fully filled out object containing the results of the task.
*
* returns: all the data you want to be stored from the task.
*/
// delete codes and coordinates from taskInputs to save space
taskInputs.forEach(function(elt, i) {
delete elt.codes;
delete elt.coordinates;
})
// task inputs to store, including parameters used
var inputs = {
params: PARAMS,
tasks: taskInputs,
subjId: SUBJ_ID,
bucketNum: BUCKET_NUM,
subjFilePath: SUBJ_FILE_PATH
}
var endTime = new Date();
// compile timing data
var timing = {
openTime: OPEN_TIME,
loadTime: LOAD_TIME,
startTime: START_TIME,
endTime: endTime,
timeToCompleteFromOpenMsec: endTime - OPEN_TIME,
timeToLoadMsec: LOAD_TIME - OPEN_TIME,
timeToCompleteFromStartMsec: endTime - START_TIME
}
// put the survey data under a separate key
var surveyData = taskOutputs.survey_data;
delete taskOutputs.survey_data;
// task outputs to store, including timing data
var new_taskoutputs = {};
for (var i = 0; i < TARGET_NUM; i++) {
new_taskoutputs[i] = taskOutputs[i];
}
var outputs = {
timing: timing,
surveyData: surveyData,
tasks: new_taskoutputs,
qa_answers: ANSWERS,
re_answers: RECO_ANSWERS
}
return {
'inputs': inputs,
'outputs': outputs
}
},
updateAnswers: function(qa_counter) {
if (qa_counter == 1) {
ANSWER_TMP = [];
}
console.log(ANSWER_TMP);
var ans = $('input:radio[name="answer"]:checked').val();
console.log(ans);
ANSWER_TMP.push(ans);
if (qa_counter == 5) {
ANSWERS = ANSWER_TMP;
}
},
recoAnswers: function() {
console.log(RECO_ANSWERS);
var ans = $('input:radio[name="re-answer"]:checked').val();
console.log(ans);
if (ans == "1" || ans == "2") {
RECO_ANSWERS.push(ans);
}
}
};
function passedTutorial() {
/* Returns false if the subject has failed the tutorial, else true.*/
var failedValidCheck = SCORES.INVALID > PARAMS.MAX_INVALID_ALLOWED_TUTORIAL;
var failedSentinelCheck = (SCORES.SENTINEL_TOTAL - SCORES.SENTINEL_CORRECT) > PARAMS.MAX_INCORRECT_SENTINELS_ALLOWED_TUTORIAL;
// check accuracy
if (failedValidCheck || failedSentinelCheck) {
$('.subtask').hide();
$('#next-button').hide();
$('#qa-button').hide();
$('#nq-button').hide();
$("#reco-button").hide();
$("#accuracy-error-message").show();
return false;
}
return true;
}
function showFixationCross(taskInput, taskIndex, taskOutput) {
/* Displays the fixation cross on the screen and queues the next target image. */
$('.subtask').hide();
$("#show-cross-subtask").show();
setTimeout(showImage.bind(this, taskInput, taskIndex, taskOutput), PARAMS.NUM_MSEC_CROSS);
}
function showImage(taskInput, taskIndex, taskOutput) {
/* Displays a target image on the screen and queues the corresponding code chart. */
console.log(taskIndex);
//console.log(ANSWERS);
$('#recall-subtask').hide();
$('.subtask').hide();
var imgFile = IMAGES[taskIndex].src;
if (!imgFile.includes('blur')) {
$("#img-blur-box").hide();
$("#img-main-box").show();
$("#img-main").attr("src", imgFile);
} else {
$("#img-blur-box").show();
$("#img-main-box").hide();
$("#img-blur").attr("src", imgFile);
}
$("#show-image-subtask").show();
var nSecs = isSentinel(taskInput[taskIndex]) ? PARAMS.NUM_MSEC_SENTINEL : PARAMS.NUM_MSEC_IMAGE;
if (taskIndex < TARGET_NUM) {
if (taskIndex === TARGET_NUM - 1) {
PARAMS.NUM_MSEC_IMAGE = 2000; //for recall stage
}
if (!imgFile.includes('blur'))
setTimeout(function() {
$(".subtask").hide();
$("#next-button").show()
}, nSecs);
// load QA
else {
$('#question-answer-subtask').show();
$('#qa-button').hide();
$('#next-button').hide();
$("#reco-button").hide();
$('#show-image-subtask').show();
$('#nq-button').show();
$('#question').html(taskInput[taskIndex].QA.Q1.question);
$('label[for=A]').html(taskInput[taskIndex].QA.Q1.A);
$('label[for=B]').html(taskInput[taskIndex].QA.Q1.B);
$('label[for=C]').html(taskInput[taskIndex].QA.Q1.C);
}
} else {
setTimeout(function() { recall_QA(taskInput, taskIndex, taskOutput); }, PARAMS.NUM_MSEC_IMAGE);
}
}
function recall_QA(taskInput, taskIndex, taskOutput) {
console.log("Recall_QA start");
$('#question-answer-subtask').hide();
$('#qa-button').hide();
$('#show-image-subtask').hide();
$('#nq-button').hide();
$('#next-button').show();
$('#recall-subtask').show();
custom.recoAnswers();
}
function preloadImages(data) {
/*
* Loads all images for the task so they are ready to go when the user starts.
*
* Shows a progress bar and disables the button to start the task until all
* images are loaded.
*
* data: task data loaded from a subject file.
*/
var continueButton = $(".instruction-button");
_disable(continueButton);
var cross = new Image(); // fixation cross image
// populates arrays to store the Image elements
data.forEach(function(elt, i) {
IMAGES.push(new Image());
});
// callback for when all images have loaded
var imLoadCallback = function() {
console.log("done loading");
_enable(continueButton);
// Once you have loaded the images and know the size, set the correct display dimensions.
// Assumes all images have the same height/width
var shouldConstrainHeight = IMAGES[0].height / IMAGES[0].width > PARAMS.IMG_HEIGHT / PARAMS.IMG_WIDTH;
if (shouldConstrainHeight) {
$(".img-box").height(PARAMS.IMG_HEIGHT);
} else {
$(".img-box").width(PARAMS.IMG_WIDTH);
}
$(".img-blur-box").height(PARAMS.IMG_HEIGHT / 2);
}
// callback for every time a single image loads
var imProgressCallback = function(imsLoaded, totalImsToLoad) {
$("#im-load-progress").progress({ 'percent': imsLoaded / totalImsToLoad * 100 })
}
onAllImagesLoaded(IMAGES.concat([cross]), imProgressCallback, imLoadCallback);
// start images loading
cross.src = FIXATION_CROSS;
$("#img-cross").attr("src", FIXATION_CROSS);
data.forEach(function(elt, i) {
IMAGES[i].src = IMAGE_BASE_PATH + elt.image;
});
}
function gupOrRandom(name, num) {
/* Returns the value for the key `name` in the querystring if it exists,
* else an int n s.t. 0 <= n < num. */
var qs = gup(name);
return qs.length > 0 ? parseInt(qs) : Math.floor(Math.random() * num);
}
function gup(name) {
/* Searches for a querystring with key name. Returns the value or "". */
var regexS = "[\\?&]" + name + "=([^&#]*)";
var regex = new RegExp(regexS);
var tmpURL = window.location.href;
var results = regex.exec(tmpURL);
if (results == null) return "";
else return results[1];
}
function clickButtonOnEnter(inputElt, buttonToClick) {
/* Set up a binding between the enter key and a specific button.
*
* If the user enters the enter key into `inputElt`, `buttonToClick`
* will be clicked.
*/
inputElt.unbind();
inputElt.focus();
// wait a little bit before re-adding the callback, otherwise
// the callback could fire twice
setTimeout(function() {
inputElt.keyup(function(event) {
if (event.keyCode === 13) { // 13 is the enter key
buttonToClick.click();
}
});
}, 500);
}
function onAllImagesLoaded(imgs, progressCallback, callback) {
/*
* Registers callbacks for when certain images are fully and partially loaded.
*
* This must be called BEFORE setting the `src` elements on imgs are set,
* else the callback could be lost.
*
* imgs: an array of Image objects to watch. They should not have already started
* loading, i.e. the `src` attribute should not yet be set.
* progressCallback: Callback to be called every time an image loads. Takes two args:
* the first is the number of images that have already loaded, the second is the total
* number of images that will be loaded.
* callback: Callback to be called when all images are loaded. Takes no args.
*
* returns: null
*/
var imsToLoad = imgs.length;
var numImsLoaded = 0;
var incrementProgress = function() {
numImsLoaded++;
progressCallback(numImsLoaded, imsToLoad);
if (numImsLoaded == imsToLoad) {
callback();
LOAD_TIME = new Date();
console.log("Time to load secs", (LOAD_TIME - OPEN_TIME) / 1000)
}
}
var successHandler = function() {
console.log("loaded an image");
incrementProgress();
}
var errorHandler = function(event) {
console.log("Error!");
}
imgs.forEach(function(elt, i) {
elt.onload = successHandler;
elt.onerror = errorHandler;
})
}
function _includes(arr, elt) {
/* Checks if array `arr` contains element `elt`. */
var idx = $.inArray(elt, arr);
return idx != -1;
}
function _disable(button) {
/* Disables button `button`. */
button.addClass('disabled');
}
function _enable(button) {
/* Enables button `button`. */
button.removeClass('disabled');
}
function isTutorial(subtask) {
/* Checks if `subtask` is part of the tutorial or not. */
return subtask.flag == "tutorial_real" || subtask.flag == "tutorial_sentinel";
}
function didEndTutorial(taskInput, taskIndex, taskOutput) {
/* Checks if the tutorial just finished. */
return !isTutorial(taskInput[taskIndex]) && (taskIndex > 0 ? isTutorial(taskInput[taskIndex - 1]) : true);
}
function isSentinel(subtask) {
/* Checks if this subtask corresponds to a sentinel image. */
return subtask.flag == "tutorial_sentinel" || subtask.flag == "sentinel";
}

View file

@ -0,0 +1,325 @@
/*
debugout.js
by @inorganik
*/
// save all the console.logs
function debugout() {
var self = this;
// OPTIONS
self.realTimeLoggingOn = true; // log in real time (forwards to console.log)
self.useTimestamps = false; // insert a timestamp in front of each log
self.useLocalStorage = false; // store the output using window.localStorage() and continuously add to the same log each session
self.recordLogs = true; // set to false after you're done debugging to avoid the log eating up memory
self.autoTrim = true; // to avoid the log eating up potentially endless memory
self.maxLines = 5000; // if autoTrim is true, this many most recent lines are saved
self.tailNumLines = 100; // how many lines tail() will retrieve
self.logFilename = 'debugout'; // filename of log downloaded with downloadLog()
self.maxDepth = 25; // max recursion depth for logged objects
// vars
self.depth = 0;
self.parentSizes = [0];
self.currentResult = '';
self.startTime = new Date();
self.output = '';
this.version = function() { return '0.5.0' }
/*
USER METHODS
*/
this.getLog = function() {
var retrievalTime = new Date();
// if recording is off, so dev knows why they don't have any logs
if (!self.recordLogs) {
self.log('[debugout.js] log recording is off.');
}
// if using local storage, get values
if (self.useLocalStorage) {
var saved = window.localStorage.getItem('debugout.js');
if (saved) {
saved = JSON.parse(saved);
self.startTime = new Date(saved.startTime);
self.output = saved.log;
retrievalTime = new Date(saved.lastLog);
}
}
return self.output +
'\n---- Log retrieved: ' + retrievalTime + ' ----\n' +
self.formatSessionDuration(self.startTime, retrievalTime);
}
// accepts optional number or uses the default for number of lines
this.tail = function(numLines) {
var numLines = numLines || self.tailLines;
return self.trimLog(self.getLog(), numLines);
}
// accepts a string to search for
this.search = function(string) {
var lines = self.output.split('\n');
var rgx = new RegExp(string);
var matched = [];
// can't use a simple Array.prototype.filter() here
// because we need to add the line number
for (var i = 0; i < lines.length; i++) {
var addr = '[' + i + '] ';
if (lines[i].match(rgx)) {
matched.push(addr + lines[i]);
}
}
var result = matched.join('\n');
if (result.length == 0) result = 'Nothing found for "' + string + '".';
return result
}
// accepts the starting line and how many lines after the starting line you want
this.getSlice = function(lineNumber, numLines) {
var lines = self.output.split('\n');
var segment = lines.slice(lineNumber, lineNumber + numLines);
return segment.join('\n');
}
// immediately downloads the log - for desktop browser use
this.downloadLog = function() {
var logFile = self.getLog();
var blob = new Blob([logFile], { type: 'data:text/plain;charset=utf-8' });
var a = document.createElement('a');
a.href = window.URL.createObjectURL(blob);
a.target = '_blank';
date = new Date();
a.download = self.logFilename + date.getTime() + '.txt';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(a.href);
}
// clears the log
this.clear = function() {
var clearTime = new Date();
self.output = '---- Log cleared: ' + clearTime + ' ----\n';
if (self.useLocalStorage) {
// local storage
var saveObject = {
startTime: self.startTime,
log: self.output,
lastLog: clearTime
}
saveObject = JSON.stringify(saveObject);
window.localStorage.setItem('debugout.js', saveObject);
}
if (self.realTimeLoggingOn) console.log('[debugout.js] clear()');
}
// records a log
this.log = function(obj) {
// log in real time
if (self.realTimeLoggingOn) console.log(obj);
// record log
var type = self.determineType(obj);
if (type != null && self.recordLogs) {
var addition = self.formatType(type, obj);
// timestamp, formatted for brevity
if (self.useTimestamps) {
var logTime = new Date();
self.output += self.formatTimestamp(logTime);
}
self.output += addition + '\n';
if (self.autoTrim) self.output = self.trimLog(self.output, self.maxLines);
// local storage
if (self.useLocalStorage) {
var last = new Date();
var saveObject = {
startTime: self.startTime,
log: self.output,
lastLog: last
}
saveObject = JSON.stringify(saveObject);
window.localStorage.setItem('debugout.js', saveObject);
}
}
self.depth = 0;
self.parentSizes = [0];
self.currentResult = '';
}
/*
METHODS FOR CONSTRUCTING THE LOG
*/
// like typeof but classifies objects of type 'object'
// kept separate from formatType() so you can use at your convenience!
this.determineType = function(object) {
if (object != null) {
var typeResult;
var type = typeof object;
if (type == 'object') {
var len = object.length;
if (len == null) {
if (typeof object.getTime == 'function') {
typeResult = 'Date';
} else if (typeof object.test == 'function') {
typeResult = 'RegExp';
} else {
typeResult = 'Object';
}
} else {
typeResult = 'Array';
}
} else {
typeResult = type;
}
return typeResult;
} else {
return null;
}
}
// format type accordingly, recursively if necessary
this.formatType = function(type, obj) {
if (self.maxDepth && self.depth >= self.maxDepth) {
return '... (max-depth reached)';
}
switch (type) {
case 'Object':
self.currentResult += '{\n';
self.depth++;
self.parentSizes.push(self.objectSize(obj));
var i = 0;
for (var prop in obj) {
self.currentResult += self.indentsForDepth(self.depth);
self.currentResult += prop + ': ';
var subtype = self.determineType(obj[prop]);
var subresult = self.formatType(subtype, obj[prop]);
if (subresult) {
self.currentResult += subresult;
if (i != self.parentSizes[self.depth] - 1) self.currentResult += ',';
self.currentResult += '\n';
} else {
if (i != self.parentSizes[self.depth] - 1) self.currentResult += ',';
self.currentResult += '\n';
}
i++;
}
self.depth--;
self.parentSizes.pop();
self.currentResult += self.indentsForDepth(self.depth);
self.currentResult += '}';
if (self.depth == 0) return self.currentResult;
break;
case 'Array':
self.currentResult += '[';
self.depth++;
self.parentSizes.push(obj.length);
for (var i = 0; i < obj.length; i++) {
var subtype = self.determineType(obj[i]);
if (subtype == 'Object' || subtype == 'Array') self.currentResult += '\n' + self.indentsForDepth(self.depth);
var subresult = self.formatType(subtype, obj[i]);
if (subresult) {
self.currentResult += subresult;
if (i != self.parentSizes[self.depth] - 1) self.currentResult += ', ';
if (subtype == 'Array') self.currentResult += '\n';
} else {
if (i != self.parentSizes[self.depth] - 1) self.currentResult += ', ';
if (subtype != 'Object') self.currentResult += '\n';
else if (i == self.parentSizes[self.depth] - 1) self.currentResult += '\n';
}
}
self.depth--;
self.parentSizes.pop();
self.currentResult += ']';
if (self.depth == 0) return self.currentResult;
break;
case 'function':
obj += '';
var lines = obj.split('\n');
for (var i = 0; i < lines.length; i++) {
if (lines[i].match(/\}/)) self.depth--;
self.currentResult += self.indentsForDepth(self.depth);
if (lines[i].match(/\{/)) self.depth++;
self.currentResult += lines[i] + '\n';
}
return self.currentResult;
break;
case 'RegExp':
return '/' + obj.source + '/';
break;
case 'Date':
case 'string':
if (self.depth > 0 || obj.length == 0) {
return '"' + obj + '"';
} else {
return obj;
}
case 'boolean':
if (obj) return 'true';
else return 'false';
case 'number':
return obj + '';
break;
}
}
this.indentsForDepth = function(depth) {
var str = '';
for (var i = 0; i < depth; i++) {
str += '\t';
}
return str;
}
this.trimLog = function(log, maxLines) {
var lines = log.split('\n');
if (lines.length > maxLines) {
lines = lines.slice(lines.length - maxLines);
}
return lines.join('\n');
}
this.lines = function() {
return self.output.split('\n').length;
}
// calculate testing time
this.formatSessionDuration = function(startTime, endTime) {
var msec = endTime - startTime;
var hh = Math.floor(msec / 1000 / 60 / 60);
var hrs = ('0' + hh).slice(-2);
msec -= hh * 1000 * 60 * 60;
var mm = Math.floor(msec / 1000 / 60);
var mins = ('0' + mm).slice(-2);
msec -= mm * 1000 * 60;
var ss = Math.floor(msec / 1000);
var secs = ('0' + ss).slice(-2);
msec -= ss * 1000;
return '---- Session duration: ' + hrs + ':' + mins + ':' + secs + ' ----'
}
this.formatTimestamp = function(timestamp) {
var year = timestamp.getFullYear();
var date = timestamp.getDate();
var month = ('0' + (timestamp.getMonth() + 1)).slice(-2);
var hrs = Number(timestamp.getHours());
var mins = ('0' + timestamp.getMinutes()).slice(-2);
var secs = ('0' + timestamp.getSeconds()).slice(-2);
return '[' + year + '-' + month + '-' + date + ' ' + hrs + ':' + mins + ':' + secs + ']: ';
}
this.objectSize = function(obj) {
var size = 0,
key;
for (key in obj) {
if (obj.hasOwnProperty(key)) size++;
}
return size;
}
/*
START/RESUME LOG
*/
if (self.useLocalStorage) {
var saved = window.localStorage.getItem('debugout.js');
if (saved) {
saved = JSON.parse(saved);
self.output = saved.log;
var start = new Date(saved.startTime);
var end = new Date(saved.lastLog);
self.output += '\n---- Session end: ' + saved.lastLog + ' ----\n';
self.output += self.formatSessionDuration(start, end);
self.output += '\n\n';
}
}
self.output += '---- Session started: ' + self.startTime + ' ----\n\n';
}

View file

@ -0,0 +1,115 @@
var demoSurvey = {
maybeLoadSurvey: function(config) {
if (config.advanced.includeDemographicSurvey) {
console.log("loading demo survey");
$('#demo-survey').load("assets/html/demo_survey.html");
$('#demo-survey').hide();
$('#feedback-field').hide();
}
},
hideSurvey: function() {
$('#demo-survey').hide();
},
showTask: function() {
// make sure to hide experiment: use the appropriate div references (or add div wrappers) to hide the previous task elements
//$('#custom-experiment').hide();
$(".subtask").hide();
// -----------------------
$('#demo-survey').show();
// Rules for collecting demographic survey data
$('#survey-form')
.form({
fields: {
gender: {
identifier: 'gender',
rules: [{
type: 'checked',
prompt: 'Please select a gender'
}]
},
ageGroup: {
identifier: 'ageGroup',
rules: [{
type: 'checked',
prompt: 'Please select an age group'
}]
},
ethnicity: {
identifier: 'ethnicity',
rules: [{
type: 'checked',
prompt: 'Please select an ethnicity'
}]
},
education: {
identifier: 'education',
rules: [{
type: 'checked',
prompt: 'Please select an education level'
}]
},
vizExperience: {
identifier: 'vizExperience',
rules: [{
type: 'checked',
prompt: 'Please select your experience with visualizations'
}]
}
}
});
$("input:checkbox[name=ethnicity]").change(function() {
var unspecified = $("#ethnicUnspecified").is(":checked");
if (unspecified) {
$("input:checkbox[name=ethnicity]").not("#ethnicUnspecified")
.prop("checked", false);
$(".ethnicityOption").addClass("disabled");
} else {
$(".ethnicityOption").removeClass("disabled");
}
});
},
collectData: function() {
var gender = $("input[type=radio][name=gender]:checked").val();
var ageGroup = $("input[type=radio][name=ageGroup]:checked").val();
var ethnicity = $("input[type=checkbox][name=ethnicity]:checked").val();
var education = $("input[type=radio][name=education]:checked").val();
var vizExperience = $("input[type=radio][name=vizExperience]:checked").val();
var feedback = htmlEscape($("textarea[name=feedback]").val());
var data = {
gender: gender,
ageGroup: ageGroup,
ethnicity: ethnicity,
education: education,
vizExperience: vizExperience,
feedback: feedback
};
return {
survey_data: data
};
},
validateTask: function() {
console.log("validating demographic survey");
$('#survey-form').form('validate form');
// falsey value indicates no error...
if (!$('#survey-form').form('is valid')) {
return {errorMessage: ""}
}
return false;
}
}
function htmlEscape(str) {
/* Html-escape a sensitive string. */
return String(str)
.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}

View file

@ -0,0 +1,500 @@
var qa_counter = 1;
var reco_flag = 0;
var config = {};
var state = {
taskIndex: gup("skipto") ? parseInt(gup("skipto")) : 0,
taskInputs: {},
taskOutputs: [],
//assignmentId: gup("assignmentId"),
assignmentId: Math.floor(Math.random() * 1000),
//workerId: gup("workerId"),
workerId: Math.floor(Math.random() * 1000),
hitId: gup("hitId")
};
// Debug log
var bugout = new debugout();
var rectBugout = new debugout();
/* HELPERS */
function saveTaskData() {
var data;
if (isDemoSurvey()) {
data = demoSurvey.collectData();
} else {
data = custom.collectData(getTaskInputs(state.taskIndex), state.taskIndex, getTaskOutputs(state.taskIndex));
}
if (config.meta.aggregate) {
$.extend(state.taskOutputs, data);
} else {
state.taskOutputs[state.taskIndex] = data;
}
}
function getTaskInputs(i) {
return config.meta.aggregate ? state.taskInputs : state.taskInputs[i];
}
``
function getTaskOutputs(i) {
return config.meta.aggregate ? state.taskOutputs : state.taskOutputs[i];
}
function updateTask() {
//console.log(state.taskIndex);
if (config.advanced.hideIfNotAccepted && hideIfNotAccepted()) {
return;
}
$("#progress-bar").progress("set progress", state.taskIndex + 1);
if (isDemoSurvey()) {
demoSurvey.showTask();
} else {
// show the user's task
demoSurvey.hideSurvey();
$('#custom-experiment').show();
custom.showTask(getTaskInputs(state.taskIndex), state.taskIndex, getTaskOutputs(state.taskIndex));
}
if (state.taskIndex == config.meta.numSubtasks + config.advanced.includeDemographicSurvey - 1) {
// last page
$("#next-button").hide();
$("#qa-button").hide();
$('#nq-button').hide();
$("#reco-button").hide();
if (state.taskIndex != 0) {
$("#prev-button").removeClass("disabled");
} else {
$("#prev-button").addClass("disabled");
}
$("#submit-button").removeClass("disabled");
$("#disclaimer").show();
$("#final-task-fields").css("display", "block"); // added this to custom.js only on the last page (last subtask) of the last task
// NOTE: comments in the above 2 lines only refer to the case where demographic survey is not shown
} else if (state.taskIndex == 0) {
// first page
$("#next-button").removeClass("disabled");
$("#prev-button").addClass("disabled");
$("#submit-button").addClass("disabled");
$("#final-task-fields").css("display", "none");
$("#disclaimer").hide();
} else {
// intermediate page
$("#next-button").removeClass("disabled");
$("#prev-button").removeClass("disabled");
$("#submit-button").addClass("disabled");
$("#final-task-fields").css("display", "none");
$("#disclaimer").hide();
}
}
function nextTask() {
spanPosition();
dd = new Date();
bugout.log('next button clicked for task:' + state.taskIndex + ', at: ' + dd);
bugout.log(dd.getTime());
console.log("moving to next task");
if (qa_counter == 5) {
custom.updateAnswers(qa_counter);
qa_counter = 1;
}
if (state.taskIndex == 79) {
custom.recoAnswers();
}
if (state.taskIndex < (config.meta.numSubtasks + config.advanced.includeDemographicSurvey) - 1) {
saveTaskData();
var failedValidation;
if (isDemoSurvey()) {
failedValidation = demoSurvey.validateTask();
} else {
failedValidation = custom.validateTask(getTaskInputs(state.taskIndex), state.taskIndex, getTaskOutputs(state.taskIndex));
}
if (failedValidation == false) {
state.taskIndex++;
updateTask();
clearMessage();
console.log("Current collected data", state.taskOutputs);
} else {
generateMessage("negative", failedValidation.errorMessage);
}
}
}
function prevTask() {
if (state.taskIndex > 0) {
saveTaskData();
state.taskIndex--;
updateTask();
}
}
function toggleInstructions() {
dd = new Date();
bugout.log('Entering recall stage: ' + dd);
bugout.log(dd.getTime());
rectBugout.log("id x y width height top right bottom left");
if ($("#experiment").css("display") == "none") {
$("#experiment").css("display", "flex");
$("#instructions").css("display", "none");
$("#disclaimer").hide();
updateTask();
} else {
saveTaskData();
$("#experiment").css("display", "none");
$("#instructions").css("display", "flex");
$("#disclaimer").show();
}
}
function clearMessage() {
$("#message-field").html("");
}
function generateMessage(cls, header) {
clearMessage();
if (!header) return;
var messageStr = "<div class='ui message " + cls + "'>";
messageStr += "<i class='close icon'></i>";
messageStr += "<div class='header'>" + header + "</div></div>";
var newMessage = $(messageStr);
$("#message-field").append(newMessage);
newMessage.click(function() {
$(this).closest(".message").transition("fade");
});
}
function addHiddenField(form, name, value) {
// form is a jQuery object, name and value are strings
var input = $("<input type='hidden' name='" + name + "' value=''>");
input.val(value);
form.append(input);
}
function submitHIT() {
console.log("submitting");
$("#copy-key-button").click(function() {
selectText('submit-code');
});
saveTaskData();
clearMessage();
$("#submit-button").addClass("loading");
for (var i = 0; i < config.meta.numSubtasks; i++) {
var failedValidation = custom.validateTask(getTaskInputs(i), i, getTaskOutputs(i));
if (failedValidation) {
cancelSubmit(failedValidation.errorMessage);
return;
}
}
if (config.advanced.includeDemographicSurvey) {
var failedValidation = demoSurvey.validateTask();
if (failedValidation) {
cancelSubmit(failedValidation.errorMessage);
return;
}
}
var results = custom.getUploadPayload(state.taskOutputs);
var payload = {
'assignmentId': state.assignmentId,
'workerId': state.workerId,
'hitId': state.hitId,
//'tag': gup('tag'),
'origin': state.origin,
'results': results
}
var submitUrl;
if (config.advanced.externalSubmit) {
submitUrl = config.advanced.externalSubmitUrl;
externalSubmit(submitUrl, payload);
} else {
submitUrl = decodeURIComponent(gup("turkSubmitTo")) + "/mturk/externalSubmit";
mturkSubmit(submitUrl, payload);
}
}
function cancelSubmit(err) {
console.log("cancelling submit");
$("#submit-button").removeClass("loading");
generateMessage("negative", err);
}
function gup(name) {
var regexS = "[\\?&]" + name + "=([^&#]*)";
var regex = new RegExp(regexS);
var tmpURL = window.location.href;
var results = regex.exec(tmpURL);
if (results == null) return "";
else return results[1];
}
/* SETUP FUNCTIONS */
function populateMetadata(config) {
$(".meta-title").html(config.meta.title);
$(".meta-desc").html(config.meta.description);
$(".instructions-simple").html(config.instructions.simple);
for (var i = 0; i < config.instructions.steps.length; i++) {
$(".instructions-steps").append($("<li>" + config.instructions.steps[i] + "</li>"));
}
$(".disclaimer-text").html(config.meta.disclaimer);
if (config.instructions.images.length > 0) {
$("#sample-task").css("display", "block");
var instructionsIndex = Math.floor(Math.random() * config.instructions.images.length);
var imgEle = "<img class='instructions-img' src='";
imgEle += config.instructions.images[instructionsIndex] + "'></img>";
$("#instructions-demo").append($(imgEle));
}
$("#progress-bar").progress({
total: config.meta.numSubtasks + config.advanced.includeDemographicSurvey,
});
}
function setupButtons() {
$("#next-button").click(nextTask);
$("#prev-button").click(prevTask);
$(".instruction-button").click(toggleInstructions);
$("#submit-button").click(submitHIT);
$("#qa-button").click(qaSubmit);
$("#nq-button").click(qaSubmit);
$("#reco-button").click(recognition_stage);
if (state.assignmentId == "ASSIGNMENT_ID_NOT_AVAILABLE") {
$("#submit-button").remove();
}
}
function recognition_stage() {
spanPosition();
dd = new Date();
bugout.log('Entering recognition stage: ' + dd);
bugout.log(dd.getTime());
if (qa_counter == 5) {
custom.updateAnswers(qa_counter);
qa_counter = 1;
}
$('#question-answer-subtask').hide();
$('#show-image-subtask').hide();
$('#reco-subtask').show();
$('#qa-button').hide();
$('#next-button').show();
$('#nq-button').hide();
$("#reco-button").hide();
}
function updateQA() {
custom.updateAnswers(qa_counter)
$('#question').html(state.taskInputs[state.taskIndex].QA['Q' + qa_counter].question);
$('label[for=A]').html(state.taskInputs[state.taskIndex].QA['Q' + qa_counter].A);
$('label[for=B]').html(state.taskInputs[state.taskIndex].QA['Q' + qa_counter].B);
$('label[for=C]').html(state.taskInputs[state.taskIndex].QA['Q' + qa_counter].C);
}
function spanPosition() {
// Span postion of answer
var allQuestion = document.getElementById("question");
var span = allQuestion.getElementsByTagName("span");
var qSkip = true;
for (j of span) {
var rectQuestion = j.getBoundingClientRect();
var rectQ = j.id + " ";
for (var key in rectQuestion) {
var item = rectQuestion[key];
if (!isNaN(item) && item != 0) {
rectQ = rectQ + item.toString() + " ";
} else if (item == 0) {
qSkip = false;
break;
}
}
// Skip recording data when the "next" button is clicked during encoding interface
if (qSkip) {
rectBugout.log(rectQ);
}
}
// Span position of answer
var allSelection = document.getElementsByClassName("selection");
aSkip = true;
for (i of allSelection) {
var span = i.getElementsByTagName("span");
for (j of span) {
var rectAnswer = j.getBoundingClientRect();
var rectA = j.id + " ";
for (var key in rectAnswer) {
var item = rectAnswer[key];
if (!isNaN(item) && item != 0) {
rectA = rectA + item.toString() + " ";
} else if (item == 0) {
aSkip = false;
break;
}
}
// Skip recording data when the "next" button is clicked during encoding interface
if (aSkip) {
rectBugout.log(rectA);
}
}
}
}
function qaSubmit() {
spanPosition();
dd = new Date();
bugout.log('QA button clicked, question No' + qa_counter + ': ' + dd);
bugout.log(dd.getTime());
if (state.taskIndex < TARGET_NUM && state.taskIndex % 4 > 1) {
if (qa_counter <= 5) {
qa_counter = qa_counter + 1;
updateQA()
$('#question-answer-subtask').show();
$('#qa-button').hide();
$("#reco-button").hide();
if (qa_counter === 5) {
if (state.taskIndex === TARGET_NUM - 1) {
$('#next-button').hide();
$('#nq-button').hide();
$('#reco-button').show();
} else {
$('#next-button').show();
$('#nq-button').hide();
}
} else {
$('#next-button').hide();
$('#nq-button').show();
}
$('#show-image-subtask').show();
}
} else {
nextTask()
}
$('#remembered-char-subtask').hide();
}
/* USEFUL HELPERS */
function isDemoSurvey() {
var useSurvey = config.advanced.includeDemographicSurvey;
var lastTask = state.taskIndex == config.meta.numSubtasks + config.advanced.includeDemographicSurvey - 1;
return useSurvey && lastTask;
}
// Hides the task UI if the user is working within an MTurk iframe and has not accepted the task
// Returns true if the task was hidden, false otherwise
function hideIfNotAccepted() {
if (state.assignmentId == "ASSIGNMENT_ID_NOT_AVAILABLE") {
console.log("Hiding if not accepted");
$('#experiment').hide();
$("#hit-not-accepted").show();
return true;
}
return false;
}
// Code to show the user's validation code; only used if task is configured as an external link
function showSubmitKey(key) {
$('#submit-code').text(key);
$('#experiment').hide();
$('#succesful-submit').show();
selectText('submit-code');
}
// highlights/selects text within an html element
// copied from:
// https://stackoverflow.com/questions/985272/selecting-text-in-an-element-akin-to-highlighting-with-your-mouse
function selectText(node) {
node = document.getElementById(node);
if (document.body.createTextRange) {
const range = document.body.createTextRange();
range.moveToElementText(node);
range.select();
document.execCommand("copy");
} else if (window.getSelection) {
const selection = window.getSelection();
const range = document.createRange();
range.selectNodeContents(node);
selection.removeAllRanges();
selection.addRange(range);
document.execCommand("copy");
} else {
console.warn("Could not select text in node: Unsupported browser.");
}
}
/* SUBMIT FUNCTIONS */
// submit to MTurk as a back-end. MTurk only accepts form submissions and frowns
// upon async POSTs.
function mturkSubmit(submitUrl, results) {
var form = $("#submit-form");
addHiddenField(form, 'assignmentId', state.assignmentId);
addHiddenField(form, 'workerId', state.workerId);
addHiddenField(form, 'results', JSON.stringify(results));
addHiddenField(form, 'feedback', $("#feedback-input").val());
$("#submit-form").attr("action", submitUrl);
$("#submit-form").attr("method", "POST");
// if (DEBUG) {
// return;
// }
$("#submit-form").submit();
$("#submit-button").removeClass("loading");
generateMessage("positive", "Thanks! Your task was submitted successfully.");
$("#submit-button").addClass("disabled");
}
// submit to a customized back-end.
function externalSubmit(submitUrl, results) {
dd = new Date();
bugout.log('Submitting study:' + dd);
bugout.log(dd.getTime());
bugout.log(results);
console.log("payload", results);
console.log("submitUrl", submitUrl);
bugout.downloadLog();
rectBugout.downloadLog();
$.ajax({
url: submitUrl,
type: 'POST',
data: JSON.stringify(results),
dataType: 'json'
}).then(function(response) {
showSubmitKey(response['key']);
}).catch(function(error) {
// This means there was an error connecting to the DEVELOPER'S
// data collection server.
// even if there is a bug/connection problem at this point,
// we want people to be paid.
// use a consistent prefix so we can pick out problem cases,
// and include their worker id so we can figure out what happened
console.log("ERROR", error);
key = "mturk_key_" + state.workerId + "_" + state.assignmentId;
showSubmitKey(key);
})
}
/* MAIN */
$(document).ready(function() {
$.getJSON("config.json").done(function(data) {
config = data;
config.meta.aggregate = true;
state.taskOutputs = {};
custom.loadTasks().done(function(taskInputData) {
config.meta.numSubtasks = taskInputData[1];
state.taskInputs = taskInputData[0];
populateMetadata(config);
demoSurvey.maybeLoadSurvey(config);
setupButtons(config);
});
});
});