/* jspsych-adaptive-category-train.js * Josh de Leeuw, Nov. 2012 * * Train subjects on a category task adaptively. * Stimuli are presented in blocks, and after a particular stimulus has been identified correctly in X * consecutive blocks, it is removed from the training list. Training ends after all stimuli have been * correctly identified in X consecutive blocks. * */ (function($) { jsPsych.adaptive_category_train = (function() { var plugin = {}; plugin.create = function(params) { trials = []; // everything is a single trial, since it is unknown how many presentations it will take. trials[0] = {}; trials[0].type = "adaptive_category_train"; trials[0].items = params["items"]; // array of all the stimuli to learn trials[0].correct_key = params["correct_key"]; // array of all the correct key responses trials[0].text_answer = params["text_answer"]; // the labels of the category members trials[0].choices = params["choices"]; // valid key responses trials[0].correct_text = params["correct_text"]; // feedback text for correct answers. trials[0].incorrect_text = params["incorrect_text"]; // feedback text for incorrect answers trials[0].consecutive_correct_min = params["consecutive_correct_min"]; // how many times they have to get the correct answer in a row for a stim. trials[0].min_percent_correct = params["min_percent_correct"] || 60; // percent correct needed to have adaptive training kick in trials[0].min_items_per_block = params["min_items_per_block"] || 5; // when remaining items is less than this #, completed items are added back in. trials[0].stop_training_criteria = params["stop_training_criteria"] || -1; // after this number of rounds below min_percent_correct, stop training. -1 = continue indefinitely. // timing trials[0].timing_display_feedback = params["timing_display_feedback"] || 1500; // default 1000ms trials[0].timing_post_trial = params["timing_post_trial"] || 1000; // default 1000ms between trials. // display progress ? trials[0].show_progress = params["show_progress"] || true; // optional parameters if(params["data"]!=undefined){ trials[0].data = params["data"]; } if(params["prompt"]!=undefined){ trials[0].prompt = params["prompt"]; } return trials; } plugin.trial = function(display_element, block, trial, part) { // create a tally for each item var all_items = []; for(var i=0; i -1 && this.blocks_under_thresh >=stop_criteria) { // end training due to failure to learn block.next(); } else { if(this.remaining_items.length > 0) { if(this.remaining_items.length < min_per_block) { shuffle(this.complete_items); for( var i = 0; this.remaining_items.length < min_per_block; i++) { this.remaining_items.push(this.complete_items[i]); } } // present remaining items in random order shuffle(this.remaining_items); var iterator = new TrialIterator(display_element, this.remaining_items, this, block, this.curr_block); this.curr_block++; var updated_trials = iterator.next(); // when this finishes, all trials are complete. // updated_trials will have the updated consecutive correct responses } else { // end training block.next(); } } } this.round_complete = function(trials) { // check items for threshold and remove items where consecutive responses has been reached var cont_trials = []; this.remaining_items = trials; var score_denominator = this.remaining_items.length; var score_numerator = 0; for(var i=0; i 0) { score_numerator++; } } var percent_correct = Math.round((score_numerator / score_denominator)*100); if(percent_correct < min_percent_correct){ this.blocks_under_thresh++; } else { this.blocks_under_thresh = 0; } for(var i=0; i=min_percent_correct){ // newly completed item this.remaining_items[i].complete = true; this.complete_items.push(this.remaining_items[i]); } else { cont_trials.push(this.remaining_items[i]); } } } } this.remaining_items = cont_trials; var remaining_objects = this.remaining_items.length; var completed_objects = this.total_items - remaining_objects; if(show_progress) { this.display_progress(completed_objects, remaining_objects, score_numerator, score_denominator); } else { // call next round this.next_round(); } } this.display_progress = function(completed_objects, remaining_objects, score_numerator, score_denominator, blocks_under_criteria) { var completed = ''; var percent_correct = Math.round((score_numerator / score_denominator)*100); if(percent_correct < min_percent_correct) { completed = '

You need to categorize at least '+min_percent_correct+'% of the items correctly in each round in order to make progress in training.

' if(stop_criteria > -1) { var remaining_blocks = stop_criteria - this.blocks_under_thresh; if(remaining_blocks >= 1){ completed += '

If you continue to have an accuracy below '+min_percent_correct+'% for '+remaining_blocks+' more round(s) of training, then training will stop and you will not be eligible for the bonus payment.

' } else { completed += '

Training will now stop because your accuracy was below '+min_percent_correct+'% for '+stop_criteria+' consecutive rounds.

' } } } else { if(remaining_objects == 0) { completed = '

Congratulations! You have completed training.

'; } else if(completed_objects > 0) { completed = '

You have correctly categorized '+completed_objects+' item(s) in '+min_correct+' consecutive rounds. You need to correctly categorize '+remaining_objects+ ' more item(s) in '+min_correct+' consecutive rounds to complete training. Items that you have correctly identified in '+min_correct+' consecutive rounds will not be shown as frequently.

'; } else { completed = '

Good job! You need to categorize an item correctly in '+min_correct+' consecutive rounds to finish training for that item. Once you have finished training for all items the next part of the experiment will begin.

'; } } display_element.html( '

You correctly categorized '+percent_correct+'% of the items in that round.

'+completed+'

Press ENTER to continue.

' ); var controller = this; var key_listener = function(e) { if(e.which=='13') { $(document).unbind('keyup',key_listener); // remove the response function, so that it doesn't get triggered again. display_element.html(''); // clear the display setTimeout(function(){controller.next_round();}, this.timing_post_trial); // call block.next() to advance the experiment after a delay. } } $(document).keyup(key_listener); } } function TrialIterator(display_element, trials, controller, block, block_idx){ this.trials = trials; this.curr_trial = 0; this.curr_block = block_idx; this.next = function() { if(this.curr_trial >= this.trials.length) { // call function in the controller controller.round_complete(trials); } else { this.do_trial(this.trials[this.curr_trial], this.curr_trial, this.curr_block); } } this.do_trial = function(trial, t_idx, b_idx) { // do the trial! // show the image display_element.append($('', { "src": trial.a_path, "class": 'cf' })); display_element.append(trial.prompt); startTime = (new Date()).getTime(); // get response var resp_func = function(e) { var flag = false; var correct = false; if(e.which==trial.correct_key) // correct category { flag = true; correct = true; } else { // check if the key is any of the options, or if it is an accidental keystroke for(var i=0;i', { "src": trial.a_path, "class": 'cf' })); // give feedback var atext = ""; if(is_correct) { atext = trial.correct_text.replace("&ANS&", trial.text_answer); } else { atext = trial.incorrect_text.replace("&ANS&", trial.text_answer); } display_element.append(atext); setTimeout(function(){finish_trial(iterator_object);}, trial.timing_display_feedback); } function finish_trial(iterator_object){ display_element.html(''); setTimeout(function(){ iterator_object.curr_trial++; iterator_object.next(); }, trial.timing_post_trial); } } } function shuffle(array) { var tmp, current, top = array.length; if(top) while(--top) { current = Math.floor(Math.random() * (top + 1)); tmp = array[current]; array[current] = array[top]; array[top] = tmp; } return array; } return plugin; })(); })(jQuery);