diff --git a/HTML/Assets/Detectors/Lumilo/Critical_Struggle/critical_struggle.js b/HTML/Assets/Detectors/Lumilo/Critical_Struggle/critical_struggle.js index 91e446c..b418b47 100644 --- a/HTML/Assets/Detectors/Lumilo/Critical_Struggle/critical_struggle.js +++ b/HTML/Assets/Detectors/Lumilo/Critical_Struggle/critical_struggle.js @@ -20,6 +20,8 @@ var mailer; // (initialize these at the bottom of this file, inside of self.onmessage) var attemptWindow; var skillLevelsAttempts; +var intervalID; +var onboardSkills; //declare and/or initialize any other custom global variables for this detector here... var stepCounter = {}; @@ -30,18 +32,22 @@ var help_variables = {"lastAction": "null", "lastHintLength": "", "lastSenseOfWhatToDo": false }; -var timerId; var timerId2; var timerId3; var timerId4; var timerId5; +var initTime; // //[optional] single out TUNABLE PARAMETERS below var windowSize = 7; var threshold = 1; +var BKTparams = {p_transit: 0.2, + p_slip: 0.1, + p_guess: 0.2, + p_know: 0.25}; var wheelSpinningAttemptThreshold = 10; //following Beck and Gong's wheel-spinning work var errorThreshold = 2; //currently arbitrary var newStepThreshold = 1; //currently arbitrary var familiarityThreshold = 0.4; var senseOfWhatToDoThreshold = 0.6; var hintIsHelpfulPlaceholder = true; //currently a dummy value (assumption that hint is always helpful...) - +var seedTime = 25; // //############################### @@ -247,15 +253,16 @@ function updateHistory(e){ function updateSkillLevelsAttempts(e, rawSkills, currStepCount){ for (var skill in rawSkills) { - - if( rawSkills[skill].name in skillLevelsAttempts ){ - if(currStepCount==1){ - skillLevelsAttempts[rawSkills[skill].name][0] += 1; + if (rawSkills.hasOwnProperty(skill)){ + if( skill in skillLevelsAttempts ){ + if(currStepCount==1){ + skillLevelsAttempts[skill][0] += 1; + } + skillLevelsAttempts[skill][1] = parseFloat(rawSkills[skill]["p_know"]); + } + else{ + skillLevelsAttempts[skill] = [1, parseFloat(rawSkills[skill]["p_know"])]; } - skillLevelsAttempts[rawSkills[skill].name][1] = parseFloat(rawSkills[skill].pKnown); - } - else{ - skillLevelsAttempts[rawSkills[skill].name] = [1, parseFloat(rawSkills[skill].pKnown)]; } } } @@ -266,6 +273,7 @@ function detect_wheel_spinning(e, rawSkills, currStepCount){ for (var skill in skillLevelsAttempts) { if ((skillLevelsAttempts[skill][0] >= 10) && (skillLevelsAttempts[skill][1] < 0.95)){ + console.log("is wheel spinning: " + skill.toString() + " " + skillLevelsAttempts[skill].toString()); return true; } } @@ -281,6 +289,92 @@ function detect_wheel_spinning(e, rawSkills, currStepCount){ //############################### // +function clone(obj) { + var copy; + + // Handle the 3 simple types, and null or undefined + if (null == obj || "object" != typeof obj) return obj; + + // Handle Date + if (obj instanceof Date) { + copy = new Date(); + copy.setTime(obj.getTime()); + return copy; + } + + // Handle Array + if (obj instanceof Array) { + copy = []; + for (var i = 0, len = obj.length; i < len; i++) { + copy[i] = clone(obj[i]); + } + return copy; + } + + // Handle Object + if (obj instanceof Object) { + copy = {}; + for (var attr in obj) { + if (obj.hasOwnProperty(attr)) copy[attr] = clone(obj[attr]); + } + return copy; + } + + throw new Error("Unable to copy obj! Its type isn't supported."); +} + + +function secondsSince(initTime){ + var currTime = new Date(); + diff = currTime.getTime() - initTime.getTime(); + console.log("time elapsed: ", diff/1000); + return (diff / 1000); +} + +function checkTimeElapsed(initTime) { + var timeDiff = secondsSince(initTime); + if( timeDiff > (300-seedTime) ){ + detector_output.history = JSON.stringify([attemptWindow, skillLevelsAttempts, initTime, onboardSkills]); + detector_output.value = "1, > 5 min" + detector_output.time = new Date(); + mailer.postMessage(detector_output); + postMessage(detector_output); + console.log("output_data = ", detector_output); + } + else if( timeDiff > (120-seedTime) ){ + detector_output.history = JSON.stringify([attemptWindow, skillLevelsAttempts, initTime, onboardSkills]); + detector_output.value = "1, > 2 min" + detector_output.time = new Date(); + mailer.postMessage(detector_output); + postMessage(detector_output); + console.log("output_data = ", detector_output); + } + else if( timeDiff > (60-seedTime) ){ + detector_output.history = JSON.stringify([attemptWindow, skillLevelsAttempts, initTime, onboardSkills]); + detector_output.value = "1, > 1 min" + detector_output.time = new Date(); + mailer.postMessage(detector_output); + postMessage(detector_output); + console.log("output_data = ", detector_output); + } + else if( timeDiff > (45-seedTime) ){ + detector_output.history = JSON.stringify([attemptWindow, skillLevelsAttempts, initTime, onboardSkills]); + detector_output.value = "1, > 45 s" + detector_output.time = new Date(); + mailer.postMessage(detector_output); + postMessage(detector_output); + console.log("output_data = ", detector_output); + } + else{ + detector_output.history = JSON.stringify([attemptWindow, skillLevelsAttempts, initTime, onboardSkills]); + detector_output.value = "1, > " + seedTime.toString() + " s" + detector_output.time = new Date(); + mailer.postMessage(detector_output); + postMessage(detector_output); + console.log("output_data = ", detector_output); + } +} + function receive_transaction( e ){ @@ -310,8 +404,44 @@ function receive_transaction( e ){ help_model_output = "preferred"; //first action in whole tutor is set to "preferred" by default } + //######## BKT ########## + var currStep = e.data.tutor_data.selection; + for (var i in currSkills){ + var skill = currSkills[i]; + + if(!(currStep in stepCounter)){ + if (!(skill in onboardSkills)){ //if this skill has not been encountered before + onboardSkills[skill] = clone(BKTparams); + } + + var p_know_tminus1 = onboardSkills[skill]["p_know"]; + var p_slip = onboardSkills[skill]["p_slip"]; + var p_guess = onboardSkills[skill]["p_guess"]; + var p_transit = onboardSkills[skill]["p_transit"]; + + console.log(onboardSkills[skill]["p_know"]); + + + if (e.data.tutor_data.action_evaluation.toLowerCase()=="correct"){ + var p_know_given_obs = (p_know_tminus1*(1-p_slip))/( (p_know_tminus1*(1-p_slip)) + ((1-p_know_tminus1)*p_guess) ); + } + else{ + var p_know_given_obs = (p_know_tminus1*p_slip)/( (p_know_tminus1*p_slip) + ((1-p_know_tminus1)*(1-p_guess)) ); + } + + onboardSkills[skill]["p_know"] = p_know_given_obs + (1 - p_know_given_obs)*p_transit; + + //following TutorShop, round down to two decimal places + onboardSkills[skill]["p_know"] = Math.floor(onboardSkills[skill]["p_know"] * 100) / 100; + + console.log("engine BKT: ", e.data.tutor_data.skills[0].pKnown); + console.log(onboardSkills[skill]["p_know"]); + } + + } + + //keep track of num attempts on each step - currStep = e.data.tool_data.selection; if(currStep in stepCounter){ stepCounter[currStep] += 1; } @@ -319,8 +449,9 @@ function receive_transaction( e ){ stepCounter[currStep] = 1; } + //######################## - var isWheelSpinning = detect_wheel_spinning(e, rawSkills, stepCounter[currStep]); + var isWheelSpinning = detect_wheel_spinning(e, onboardSkills, stepCounter[currStep]); attemptWindow.shift(); attemptWindow.push( (help_model_output == "ask teacher for help/try step" || isWheelSpinning) ? 1 : 0 ); @@ -342,57 +473,24 @@ function receive_transaction( e ){ //custom processing (insert code here) if (detector_output.value=="0, > 0 s" && (sumAskTeacherForHelp >= threshold)){ - detector_output.history = JSON.stringify([attemptWindow, skillLevelsAttempts]); - detector_output.value = "1, > 25 s" + initTime = new Date(); + detector_output.history = JSON.stringify([attemptWindow, skillLevelsAttempts, initTime, onboardSkills]); + detector_output.value = "1, > " + seedTime.toString() + " s"; detector_output.time = new Date(); - timerId = setTimeout(function() { - detector_output.history = JSON.stringify([attemptWindow, skillLevelsAttempts]); - detector_output.value = "1, > 45 s" - detector_output.time = new Date(); - mailer.postMessage(detector_output); - postMessage(detector_output); - console.log("output_data = ", detector_output); }, - 20000) - timerId2 = setTimeout(function() { - detector_output.history = JSON.stringify([attemptWindow, skillLevelsAttempts]); - detector_output.value = "1, > 1 min" - detector_output.time = new Date(); - mailer.postMessage(detector_output); - postMessage(detector_output); - console.log("output_data = ", detector_output); }, - 35000) - timerId3 = setTimeout(function() { - detector_output.history = JSON.stringify([attemptWindow, skillLevelsAttempts]); - detector_output.value = "1, > 2 min" - detector_output.time = new Date(); - mailer.postMessage(detector_output); - postMessage(detector_output); - console.log("output_data = ", detector_output); }, - 95000) - timerId4 = setTimeout(function() { - detector_output.history = JSON.stringify([attemptWindow, skillLevelsAttempts]); - detector_output.value = "1, > 5 min" - detector_output.time = new Date(); - mailer.postMessage(detector_output); - postMessage(detector_output); - console.log("output_data = ", detector_output); }, - 275000) + intervalID = setInterval( function() { checkTimeElapsed(initTime);} , 3000); } else if (detector_output.value!="0, > 0 s" && (sumAskTeacherForHelp >= threshold)){ - detector_output.history = JSON.stringify([attemptWindow, skillLevelsAttempts]); + detector_output.history = JSON.stringify([attemptWindow, skillLevelsAttempts, initTime, onboardSkills]); detector_output.time = new Date(); } else{ detector_output.value = "0, > 0 s"; - detector_output.history = JSON.stringify([attemptWindow, skillLevelsAttempts]); + detector_output.history = JSON.stringify([attemptWindow, skillLevelsAttempts, initTime, onboardSkills]); detector_output.time = new Date(); - clearTimeout(timerId); - clearTimeout(timerId2); - clearTimeout(timerId3); - clearTimeout(timerId4); + clearInterval(intervalID); } @@ -445,6 +543,7 @@ self.onmessage = function ( e ) { // attemptWindow = Array.apply(null, Array(windowSize)).map(Number.prototype.valueOf,0); skillLevelsAttempts = {}; + onboardSkills = {}; } else{ //if the detector history is not empty, you can access it via: @@ -455,6 +554,12 @@ self.onmessage = function ( e ) { var all_history = JSON.parse(detector_output.history); attemptWindow = all_history[0]; skillLevelsAttempts = all_history[1]; + initTime = new Date(all_history[2]); + onboardSkills = all_history[3]; + + if(detector_output.value!="0, > 0 s"){ + intervalID = setInterval( function() { checkTimeElapsed(initTime);} , 3000); + } } break; diff --git a/HTML/Assets/Detectors/Lumilo/System_Misuse/system_misuse.js b/HTML/Assets/Detectors/Lumilo/System_Misuse/system_misuse.js index e6b3138..32414ca 100644 --- a/HTML/Assets/Detectors/Lumilo/System_Misuse/system_misuse.js +++ b/HTML/Assets/Detectors/Lumilo/System_Misuse/system_misuse.js @@ -34,7 +34,7 @@ var timerId; var timerId2; var timerId3; var timerId4; var timerId5; //[optional] single out TUNABLE PARAMETERS below var windowSize = 7; //arbitrary: need to tune var threshold = 1; //arbitrary: need to tune -var errorThreshold = 2; //currently arbitrary +var errorThreshold = 1; //currently arbitrary var newStepThreshold = 1; //currently arbitrary var familiarityThreshold = 0.4; var senseOfWhatToDoThreshold = 0.6; diff --git a/HTML/Assets/Detectors/bkt_models/BKT.js b/HTML/Assets/Detectors/bkt_models/BKT.js index 55dbdcc..3c00bd2 100644 --- a/HTML/Assets/Detectors/bkt_models/BKT.js +++ b/HTML/Assets/Detectors/bkt_models/BKT.js @@ -7,7 +7,7 @@ var variableName = "BKT" var detector_output = {name: variableName, category: "Dashboard", value: "", - history: {}, + history: "", skill_names: "", step_id: "", transaction_id: "", @@ -19,7 +19,10 @@ var mailer; //declare any custom global variables that will be initialized //based on "remembered" values across problem boundaries, here // (initialize these at the bottom of this file, inside of self.onmessage) -var BKTparams; +var BKTparams = {p_transit: 0.2, + p_slip: 0.1, + p_guess: 0.2, + p_know: 0.25}; //declare and/or initialize any other custom global variables for this detector here... var pastSteps = {}; @@ -62,14 +65,6 @@ function clone(obj) { } -//TO-DO: -// detector initialiization, and leave comment -// showing user how not to initialize (or, if we decide to -// initialize all detector variables by default, at startup... -// I suppose this would mean showing user how to clear initialized -// values upon the first transaction received?) - - function receive_transaction( e ){ //e is the data of the transaction from mailer from transaction assembler @@ -115,6 +110,9 @@ function receive_transaction( e ){ detector_output.history[skill]["p_know"] = p_know_given_obs + (1 - p_know_given_obs)*p_transit; + //following TutorShop, round down to two decimal places + detector_output.history[skill]["p_know"] = Math.floor(detector_output.history[skill]["p_know"] * 100) / 100; + console.log("engine BKT: ", e.data.tutor_data.skills[0].pKnown); console.log(detector_output.history[skill]["p_know"]); } @@ -133,9 +131,11 @@ function receive_transaction( e ){ if(e.data.actor == 'student' && e.data.tool_data.action != "UpdateVariable"){ detector_output.time = new Date(); detector_output.transaction_id = e.data.transaction_id; + detector_output.history = JSON.stringify(detector_output.history); mailer.postMessage(detector_output); postMessage(detector_output); console.log("output_data = ", detector_output); + detector_output.history = JSON.parse(detector_output.history); } } @@ -180,10 +180,8 @@ self.onmessage = function ( e ) { //in the event that the detector history is empty, //initialize variables to your desired 'default' values // - BKTparams = {p_transit: 0.2, - p_slip: 0.1, - p_guess: 0.2, - p_know: 0.3}; + + detector_output.history = {}; } else{ //if the detector history is not empty, you can access it via: @@ -191,7 +189,7 @@ self.onmessage = function ( e ) { //...and initialize your variables to your desired values, based on //this history // - BKTparams = JSON.parse(detector_output.history); + detector_output.history = JSON.parse(detector_output.history); } break; diff --git a/HTML/Assets/networking_for_replay/Master_Lynnette_Load_Tester.js b/HTML/Assets/networking_for_replay/Master_Lynnette_Load_Tester.js new file mode 100644 index 0000000..f5899f3 --- /dev/null +++ b/HTML/Assets/networking_for_replay/Master_Lynnette_Load_Tester.js @@ -0,0 +1,369 @@ +/* + For testing the dashboard. + Has functions for the outer part of the test page. + Include this with Lynnette_Load_Tester.html + -Peter +*/ +var studentScript; +var map; +var mapHeader; +var scriptIndex; +var myFrame; +var inChangeProblem = false; +var student = 1; +var isFirstProblem = true; +var init; +//var saiTableContents= ''; + +var initWorker = new SharedWorker('initWorker.js'); + +initWorker.port.start(); + + +$(document).on("ready",function(){ + getStudentScript(); + + getMap(); + + $("#student_info").html("Student: "+student); + + var saiTableContents = "
"; + document.getElementById("saiTable").srcdoc = saiTableContents; + $("#saiTable").hide(); + + makeCenter(); + + beginLoadTester(); +}); + +function beginLoadTester() +{ + myFrame = document.createElement("IFRAME"); + changeProblem(); +} +function changeProblem1(){ + /* Solves this strange issue where when the problem is finished, + ctat.min.js tries to get the next problem. This 2 second wait + allows for this ctat.min.js call to fail, and then we send the real + call to get the next tutor. Temporary fix, something better needs + to be implemented */ + setTimeout(function(){changeProblem();},2000); +} +function changeProblem() +{ + if( inChangeProblem == true ){ + return; + } + + $('#saiTableHolder').text(''); + $('#saiTableHolder').hide(); + $("#saiTable").contents().find('table').html(""); + $("#saiTable").hide(); + + inChangeProblem = true; + + interfaceData = getDataForInterface( studentScript[scriptIndex] ); + + var pkg = interfaceData['pkg']; + var ps = interfaceData['ps']; + var prob = interfaceData['prob'].replace(" ","+"); + var school = interfaceData['school']; + var className = interfaceData['class']; + var assignment = interfaceData['assgn']; + + $("#problem_info").html("Problem Name: "+prob+"
Problem Set: "+ps+"
Package: "+pkg); + + $("#tutorFrame").html(''); + myFrame = document.createElement("IFRAME"); + myFrame.setAttribute("id","tutor"); + $("#tutorFrame").append(myFrame); + $("#tutor").css("width",$("#tutorFrame").css("width")); + $("#tutor").css("height",$("#tutorFrame").css("height")); + +//run_replay_student_assignment///(/)?user_id= + + //var forFrame = "https://dashboard.fractions.cs.cmu.edu/run_replay_student_assignment/"+ + var forFrame = '/run_replay_student_assignment/' + + encodeURIComponent(pkg) + "/"+encodeURIComponent(ps) + "/" + + encodeURIComponent(prob) + "?user_id=" + encodeURIComponent("test_student_" + student) + + "&school_name=" + encodeURIComponent(school) + "&class_name=" + encodeURIComponent(className) + + "&assignment_name=" + encodeURIComponent(assignment + ' (Reply)') + + (isFirstProblem ? "&first=true" : "&first=true"); + + console.log(forFrame); + + var xhttp = new XMLHttpRequest(); + xhttp.onreadystatechange = function() { + if (xhttp.readyState == 4 && xhttp.status == 200) { + forFrame = xhttp.responseText.split('\n'); + } + }; + xhttp.open("GET", forFrame, false); + xhttp.send(); + + if( (forFrame+"").substring(0,5) == "')[1].split('')[0]; + + var mapData = map[probName]; + var pkg = mapData[mapHeader.indexOf('Package')]; + var ps = mapData[mapHeader.indexOf('Level (ProblemSet)')]; + var assgn = mapData[mapHeader.indexOf('Level (Assignment)')]; + var school = ctxMess.split('')[1].split('')[0]; + var className = ctxMess.split('')[1].split('')[0]; + + var ret = {}; + ret['pkg'] = pkg; + ret['ps'] = ps; + ret['prob'] = probName; + ret['assgn'] = assgn; + ret['school'] = school; + ret['class'] = className; + + return ret; +} + +/** + Called by LoadTester.js to get a single script that contains all the messages + in one replay unit. +*/ +function getNextScript() +{ + //console.log('scriptIndex = '+scriptIndex); + var replayUnitScript = [] + for( var i = 0; scriptIndex < studentScript.length; scriptIndex++ ) + { + var line = studentScript[scriptIndex] + + if(line.length < 15){ break; } + + if(line.substring(0,15) == "'+ + "\t"+a+""+i+"
"; + var newline = '

'; + $("#saiTable").contents().find('table').prepend(newline); + + //The timer is intended to make a little space, have it display for a + //small period of time, then add the sai. + setTimeout(function(){ + $("#saiTable").contents().find('#deletethis').remove(); + $("#saiTable").contents().find('table').prepend(newEntry); + },200); +} + +/** + Make the elements on the page centered. +*/ +$(window).on("resize", function (){ + makeCenter(); +}); +function makeCenter() +{ + var width = window.innerWidth; + var height = window.innerHeight; + var currSize = parseInt($("#tutorFrame").css("width")); + var marginLeft = (width-currSize)/2; + if (marginLeft < 0){marginLeft = 0;} + $("#tutorFrame").css("margin-left",marginLeft); + + currSize = parseInt($("#replayInfo").css("width")); + marginLeft = (width-currSize)/2; + if (marginLeft < 0){marginLeft = 0;} + $("#replayInfo").css("margin-left",marginLeft); + + currSize = parseInt($("#saiTable").css("width")); + marginLeft = (width-currSize)/2; + if (marginLeft < 0){marginLeft = 0;} + $("#saiTable").css("margin-left",marginLeft); + + currSize = parseInt($("#saiTableHolder").css("width")); + if (currSize > 0){ + marginLeft = (width-currSize)/2; + if (marginLeft < 0){marginLeft = 0;} + $("#saiTableHolder").css("margin-left",marginLeft); + } +} + +/** + Checks control.txt on the server to see if the tutor should be paused. + returns true if paused, false if not. +*/ +function isPaused() +{ + var dummyTimestamp = new Date().getTime(); + var ret; + var xhttp = new XMLHttpRequest(); + xhttp.onreadystatechange = function() { + if (xhttp.readyState == 4 && xhttp.status == 200) { + var data = xhttp.responseText; + if( data == "paused" ){ + var pa = 'pause'; + ret = true; + } + else{ + var pl = 'play'; + ret = false; + } + } + }; + xhttp.open("GET", "/replay/pause/control.txt" + '?ts=' + dummyTimestamp, false); + xhttp.send(); + return ret; +} + +/** + This is to make the tutor appear smaller if using ManyTutors.html +*/ +var tutorWidth = 0; +$(document).on("ready",function(){ + var withpx = $("#tutor").css("width"); + tutorWidth = parseInt(withpx.substring(0,withpx.length-2)); + if( inIframe() ){ + resize(); + $(window).on("resize",function(){ + resize(); + }); + } +}); +function inIframe () { + try { + return window.self !== window.top; + } catch (e) { + return true;} +} +function resize(){ + var wind = window.innerWidth; + var ratio = (wind / tutorWidth)*.93; + $("body").css('transform','scale('+ratio+','+ratio+')'); + $("body").css('transform-origin',' 0 0'); +} + + +/** + When you exit out of the student's replay, take your student number off of the + list of active students. +*/ +$(window).on("beforeunload", function(){ + var xhttp = new XMLHttpRequest(); + xhttp.onreadystatechange = function() { + if (xhttp.readyState == 4 && xhttp.status == 200) { + //console.log(xhttp.responseText); + } + }; + xhttp.open("GET", "/replay/pause/removeActiveStudent.php?student="+student, false); + xhttp.send(); +}); + + + + +/** + UPDATE: This is not currently used at all. Use it if you want to send a particular + problem summary + Called by the tutor UI upon being unloaded. + Sends problem summary, and then calls changeProblem() +*/ +function endProblem(problemSummary){ + var url = "/process_replay_student_assignment/0/0"; //ToDo: change parameters + + //$.post(url, problemSummary, function(result){console.log('result='+result);}); + //Problem Summary is most likely sent already by the tutor + + changeProblem(); +} diff --git a/HTML/Assets/networking_for_replay/transaction_mailer_users.js b/HTML/Assets/networking_for_replay/transaction_mailer_users.js index 6dacb97..c2539d0 100644 --- a/HTML/Assets/networking_for_replay/transaction_mailer_users.js +++ b/HTML/Assets/networking_for_replay/transaction_mailer_users.js @@ -11,7 +11,8 @@ TransactionMailerUsers = "Detectors/Lumilo/struggle__moving_average.js", "Detectors/Lumilo/student_doing_well__moving_average.js", "Detectors/Lumilo/critical_struggle.js", - "Detectors/Lumilo/invisible_hand_raise.js"], + "Detectors/Lumilo/invisible_hand_raise.js", + "Detectors/BKT.js"], active: [] }; diff --git a/HTML/Assets/transaction_mailer_users.js b/HTML/Assets/transaction_mailer_users.js index 719506f..fc722a5 100644 --- a/HTML/Assets/transaction_mailer_users.js +++ b/HTML/Assets/transaction_mailer_users.js @@ -6,12 +6,12 @@ TransactionMailerUsers = mailerURL: "mail-worker.js", mailer: null, mailerPort: null, - scripts: ["Detectors/Lumilo/Idle/idle.js", - "Detectors/Lumilo/System_Misuse/system_misuse.js", - "Detectors/Lumilo/Struggle/struggle__moving_average.js", - "Detectors/Lumilo/Student_Doing_Well/student_doing_well__moving_average.js", - "Detectors/Lumilo/Critical_Struggle/critical_struggle.js", - "Detectors/Lumilo/Invisible_Hand_Raise/invisible_hand_raise.js"], + scripts: ["Detectors/Lumilo/idle.js", + "Detectors/Lumilo/system_misuse.js", + "Detectors/Lumilo/struggle__moving_average.js", + "Detectors/Lumilo/student_doing_well__moving_average.js", + "Detectors/Lumilo/critical_struggle.js", + "Detectors/Lumilo/invisible_hand_raise.js"], active: [] };