diff --git a/amd/build/append_fourm_post.min.js b/amd/build/append_fourm_post.min.js
index ea245724..7b9da09b 100644
--- a/amd/build/append_fourm_post.min.js
+++ b/amd/build/append_fourm_post.min.js
@@ -1,3 +1,3 @@
-define("tiny_cursive/append_fourm_post",["jquery","core/ajax","core/str","core/templates","./replay","./analytic_button","./replay_button","./analytic_events"],(function($,AJAX,str,templates,Replay,analyticButton,replayButton,AnalyticEvents){const replayInstances={};window.video_playback=function(mid,filepath){if(""!==filepath){const replay=new Replay("content"+mid,filepath,10,!1,"player_"+mid);replayInstances[mid]=replay}else templates.render("tiny_cursive/no_submission").then((html=>($("#content"+mid).html(html),!0))).catch((e=>window.console.error(e)));return!1};var usersTable={init:function(scoreSetting,showcomment,hasApiKey){str.get_strings([{key:"field_require",component:"tiny_cursive"}]).done((function(){usersTable.getToken(scoreSetting,showcomment,hasApiKey)}))},getToken:function(scoreSetting,showcomment,hasApiKey){$("#page-mod-forum-discuss").find("article").get().forEach((function(entry){var replyButton=$('a[data-region="post-action"][title="Reply"]');replyButton.length>0&&replyButton.on("click",(function(event){event.preventDefault();var url=$(this).attr("href");window.location.href=url}));var ids=$("#"+entry.id).data("post-id"),cmid=M.cfg.contextInstanceId;let args={id:ids,modulename:"forum",cmid:cmid},com=AJAX.call([{methodname:"cursive_get_forum_comment_link",args:args}]);return com[0].done((function(json){var data=JSON.parse(json),filepath="";if(data.data.filename&&(filepath=data.data.filename),filepath){let analyticButtonDiv=document.createElement("div");hasApiKey?analyticButtonDiv.append(analyticButton(data.data.effort_ratio,ids)):$(analyticButtonDiv).html(replayButton(ids)),analyticButtonDiv.classList.add("text-center","my-2"),analyticButtonDiv.dataset.region="analytic-div"+ids,$("#"+entry.id).find("#post-content-"+ids).prepend(analyticButtonDiv);let myEvents=new AnalyticEvents;var context={tabledata:data.data,formattime:myEvents.formatedTime(data.data),page:scoreSetting,userid:ids,apikey:hasApiKey};let authIcon=myEvents.authorshipStatus(data.data.first_file,data.data.score,scoreSetting);myEvents.createModal(ids,context,"",replayInstances,authIcon),myEvents.analytics(ids,templates,context,"",replayInstances,authIcon),myEvents.checkDiff(ids,data.data.file_id,"",replayInstances),myEvents.replyWriting(ids,filepath,"",replayInstances)}})),com.usercomment}))}};return usersTable}));
+define("tiny_cursive/append_fourm_post",["jquery","core/ajax","core/str","core/templates","./replay","./analytic_button","./replay_button","./analytic_events"],(function($,AJAX,str,templates,Replay,analyticButton,replayButton,AnalyticEvents){const replayInstances={};window.video_playback=function(mid,filepath){if(""!==filepath){const replay=new Replay("content"+mid,filepath,10,!1,"player_"+mid);replayInstances[mid]=replay}else templates.render("tiny_cursive/no_submission").then((html=>($("#content"+mid).html(html),!0))).catch((e=>window.console.error(e)));return!1};var usersTable={init:function(scoreSetting,showcomment,hasApiKey){str.get_strings([{key:"field_require",component:"tiny_cursive"}]).done((function(){usersTable.getToken(scoreSetting,showcomment,hasApiKey)}))},getToken:function(scoreSetting,showcomment,hasApiKey){$("#page-mod-forum-discuss").find("article").get().forEach((function(entry){var replyButton=$('a[data-region="post-action"][title="Reply"]');replyButton.length>0&&replyButton.on("click",(function(event){if($("#body").hasClass("teacher_admin"))return!0;event.preventDefault();var urlParts=$(this).attr("href").split("#"),baseUrl=urlParts[0],hash=urlParts.length>1?"#"+urlParts[1]:"";baseUrl.indexOf("setformat=")>-1?baseUrl=baseUrl.replace(/setformat=\d/,"setformat=1"):baseUrl.indexOf("?")>-1?baseUrl+="&setformat=1":baseUrl+="?setformat=1";var finalUrl=baseUrl+hash;window.location.href=finalUrl}));var ids=$("#"+entry.id).data("post-id"),cmid=M.cfg.contextInstanceId;let args={id:ids,modulename:"forum",cmid:cmid},com=AJAX.call([{methodname:"cursive_get_forum_comment_link",args:args}]);return com[0].done((function(json){var data=JSON.parse(json),filepath="";if(data.data.filename&&(filepath=data.data.filename),filepath){let analyticButtonDiv=document.createElement("div");hasApiKey?analyticButtonDiv.append(analyticButton(data.data.effort_ratio,ids)):$(analyticButtonDiv).html(replayButton(ids)),analyticButtonDiv.classList.add("text-center","my-2"),analyticButtonDiv.dataset.region="analytic-div"+ids,$("#"+entry.id).find("#post-content-"+ids).prepend(analyticButtonDiv);let myEvents=new AnalyticEvents;var context={tabledata:data.data,formattime:myEvents.formatedTime(data.data),page:scoreSetting,userid:ids,apikey:hasApiKey};let authIcon=myEvents.authorshipStatus(data.data.first_file,data.data.score,scoreSetting);myEvents.createModal(ids,context,"",replayInstances,authIcon),myEvents.analytics(ids,templates,context,"",replayInstances,authIcon),myEvents.checkDiff(ids,data.data.file_id,"",replayInstances),myEvents.replyWriting(ids,filepath,"",replayInstances)}})),com.usercomment}))}};return usersTable}));
//# sourceMappingURL=append_fourm_post.min.js.map
\ No newline at end of file
diff --git a/amd/build/append_fourm_post.min.js.map b/amd/build/append_fourm_post.min.js.map
index 3f491c81..bada2509 100644
--- a/amd/build/append_fourm_post.min.js.map
+++ b/amd/build/append_fourm_post.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"append_fourm_post.min.js","sources":["../src/append_fourm_post.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * @module tiny_cursive/append_fourm_post\n * @category TinyMCE Editor\n * @copyright CTI \n * @author kuldeep singh \n */\n\ndefine([\"jquery\", \"core/ajax\", \"core/str\", \"core/templates\", \"./replay\", \"./analytic_button\",\n \"./replay_button\", \"./analytic_events\"], function(\n $,\n AJAX,\n str,\n templates,\n Replay,\n analyticButton,\n replayButton,\n AnalyticEvents\n) {\n const replayInstances = {};\n // eslint-disable-next-line camelcase\n window.video_playback = function(mid, filepath) {\n if (filepath !== '') {\n const replay = new Replay(\n 'content' + mid,\n filepath,\n 10,\n false,\n 'player_' + mid\n );\n replayInstances[mid] = replay;\n } else {\n templates.render('tiny_cursive/no_submission').then(html => {\n $('#content' + mid).html(html);\n return true;\n }).catch(e => window.console.error(e));\n }\n return false;\n\n };\n\n var usersTable = {\n init: function(scoreSetting, showcomment, hasApiKey) {\n str\n .get_strings([\n {key: \"field_require\", component: \"tiny_cursive\"},\n ])\n .done(function() {\n usersTable.getToken(scoreSetting, showcomment, hasApiKey);\n });\n },\n getToken: function(scoreSetting, showcomment, hasApiKey) {\n $('#page-mod-forum-discuss').find(\"article\").get().forEach(function(entry) {\n var replyButton = $('a[data-region=\"post-action\"][title=\"Reply\"]');\n if (replyButton.length > 0) {\n replyButton.on('click', function(event) {\n event.preventDefault();\n var url = $(this).attr('href');\n window.location.href = url;\n });\n }\n\n var ids = $(\"#\" + entry.id).data(\"post-id\");\n var cmid = M.cfg.contextInstanceId;\n\n let args = {id: ids, modulename: \"forum\", cmid: cmid};\n let methodname = 'cursive_get_forum_comment_link';\n let com = AJAX.call([{methodname, args}]);\n com[0].done(function(json) {\n var data = JSON.parse(json);\n\n var filepath = '';\n if (data.data.filename) {\n filepath = data.data.filename;\n }\n if (filepath) {\n\n let analyticButtonDiv = document.createElement('div');\n\n if (!hasApiKey) {\n $(analyticButtonDiv).html(replayButton(ids));\n } else {\n analyticButtonDiv.append(analyticButton(data.data.effort_ratio, ids));\n }\n\n analyticButtonDiv.classList.add('text-center', 'my-2');\n analyticButtonDiv.dataset.region = \"analytic-div\" + ids;\n\n $(\"#\" + entry.id).find('#post-content-' + ids).prepend(analyticButtonDiv);\n\n let myEvents = new AnalyticEvents();\n var context = {\n tabledata: data.data,\n formattime: myEvents.formatedTime(data.data),\n page: scoreSetting,\n userid: ids,\n apikey: hasApiKey\n };\n\n let authIcon = myEvents.authorshipStatus(data.data.first_file, data.data.score, scoreSetting);\n myEvents.createModal(ids, context, '', replayInstances, authIcon);\n myEvents.analytics(ids, templates, context, '', replayInstances, authIcon);\n myEvents.checkDiff(ids, data.data.file_id, '', replayInstances);\n myEvents.replyWriting(ids, filepath, '', replayInstances);\n }\n\n });\n return com.usercomment;\n });\n },\n };\n return usersTable;\n\n\n});"],"names":["define","$","AJAX","str","templates","Replay","analyticButton","replayButton","AnalyticEvents","replayInstances","window","video_playback","mid","filepath","replay","render","then","html","catch","e","console","error","usersTable","init","scoreSetting","showcomment","hasApiKey","get_strings","key","component","done","getToken","find","get","forEach","entry","replyButton","length","on","event","preventDefault","url","this","attr","location","href","ids","id","data","cmid","M","cfg","contextInstanceId","args","modulename","com","call","methodname","json","JSON","parse","filename","analyticButtonDiv","document","createElement","append","effort_ratio","classList","add","dataset","region","prepend","myEvents","context","tabledata","formattime","formatedTime","page","userid","apikey","authIcon","authorshipStatus","first_file","score","createModal","analytics","checkDiff","file_id","replyWriting","usercomment"],"mappings":"AAsBAA,wCAAO,CAAC,SAAU,YAAa,WAAY,iBAAkB,WAAY,oBACrE,kBAAmB,sBAAsB,SACzCC,EACAC,KACAC,IACAC,UACAC,OACAC,eACAC,aACAC,sBAEMC,gBAAkB,GAExBC,OAAOC,eAAiB,SAASC,IAAKC,aACjB,KAAbA,SAAiB,OACXC,OAAS,IAAIT,OACf,UAAYO,IACZC,SACA,IACA,EACA,UAAYD,KAEhBH,gBAAgBG,KAAOE,YAEvBV,UAAUW,OAAO,8BAA8BC,MAAKC,OAChDhB,EAAE,WAAaW,KAAKK,KAAKA,OAClB,KACRC,OAAMC,GAAKT,OAAOU,QAAQC,MAAMF,YAEhC,OAIPG,WAAa,CACbC,KAAM,SAASC,aAAcC,YAAaC,WACtCvB,IACKwB,YAAY,CACT,CAACC,IAAK,gBAAiBC,UAAW,kBAErCC,MAAK,WACFR,WAAWS,SAASP,aAAcC,YAAaC,eAG3DK,SAAU,SAASP,aAAcC,YAAaC,WAC1CzB,EAAE,2BAA2B+B,KAAK,WAAWC,MAAMC,SAAQ,SAASC,WAC5DC,YAAcnC,EAAE,+CAChBmC,YAAYC,OAAS,GACrBD,YAAYE,GAAG,SAAS,SAASC,OAC7BA,MAAMC,qBACFC,IAAMxC,EAAEyC,MAAMC,KAAK,QACvBjC,OAAOkC,SAASC,KAAOJ,WAI3BK,IAAM7C,EAAE,IAAMkC,MAAMY,IAAIC,KAAK,WAC7BC,KAAOC,EAAEC,IAAIC,sBAEbC,KAAO,CAACN,GAAID,IAAKQ,WAAY,QAASL,KAAMA,MAE5CM,IAAMrD,KAAKsD,KAAK,CAAC,CAACC,WADL,iCACiBJ,KAAAA,eAClCE,IAAI,GAAGzB,MAAK,SAAS4B,UACbV,KAAOW,KAAKC,MAAMF,MAElB7C,SAAW,MACXmC,KAAKA,KAAKa,WACVhD,SAAWmC,KAAKA,KAAKa,UAErBhD,SAAU,KAENiD,kBAAoBC,SAASC,cAAc,OAE1CtC,UAGDoC,kBAAkBG,OAAO3D,eAAe0C,KAAKA,KAAKkB,aAAcpB,MAFhE7C,EAAE6D,mBAAmB7C,KAAKV,aAAauC,MAK3CgB,kBAAkBK,UAAUC,IAAI,cAAe,QAC/CN,kBAAkBO,QAAQC,OAAS,eAAiBxB,IAEpD7C,EAAE,IAAMkC,MAAMY,IAAIf,KAAK,iBAAmBc,KAAKyB,QAAQT,uBAEnDU,SAAW,IAAIhE,mBACfiE,QAAU,CACVC,UAAW1B,KAAKA,KAChB2B,WAAYH,SAASI,aAAa5B,KAAKA,MACvC6B,KAAMrD,aACNsD,OAAQhC,IACRiC,OAAQrD,eAGRsD,SAAWR,SAASS,iBAAiBjC,KAAKA,KAAKkC,WAAYlC,KAAKA,KAAKmC,MAAO3D,cAChFgD,SAASY,YAAYtC,IAAK2B,QAAS,GAAIhE,gBAAiBuE,UACxDR,SAASa,UAAUvC,IAAK1C,UAAWqE,QAAS,GAAIhE,gBAAiBuE,UACjER,SAASc,UAAUxC,IAAKE,KAAKA,KAAKuC,QAAS,GAAI9E,iBAC/C+D,SAASgB,aAAa1C,IAAKjC,SAAU,GAAIJ,qBAI1C8C,IAAIkC,wBAIhBnE"}
\ No newline at end of file
+{"version":3,"file":"append_fourm_post.min.js","sources":["../src/append_fourm_post.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * @module tiny_cursive/append_fourm_post\n * @category TinyMCE Editor\n * @copyright CTI \n * @author kuldeep singh \n */\n\ndefine([\"jquery\", \"core/ajax\", \"core/str\", \"core/templates\", \"./replay\", \"./analytic_button\",\n \"./replay_button\", \"./analytic_events\"], function(\n $,\n AJAX,\n str,\n templates,\n Replay,\n analyticButton,\n replayButton,\n AnalyticEvents\n) {\n const replayInstances = {};\n // eslint-disable-next-line camelcase\n window.video_playback = function(mid, filepath) {\n if (filepath !== '') {\n const replay = new Replay(\n 'content' + mid,\n filepath,\n 10,\n false,\n 'player_' + mid\n );\n replayInstances[mid] = replay;\n } else {\n templates.render('tiny_cursive/no_submission').then(html => {\n $('#content' + mid).html(html);\n return true;\n }).catch(e => window.console.error(e));\n }\n return false;\n\n };\n\n var usersTable = {\n init: function(scoreSetting, showcomment, hasApiKey) {\n str\n .get_strings([\n {key: \"field_require\", component: \"tiny_cursive\"},\n ])\n .done(function() {\n usersTable.getToken(scoreSetting, showcomment, hasApiKey);\n });\n },\n getToken: function(scoreSetting, showcomment, hasApiKey) {\n $('#page-mod-forum-discuss').find(\"article\").get().forEach(function(entry) {\n var replyButton = $('a[data-region=\"post-action\"][title=\"Reply\"]');\n if (replyButton.length > 0) {\n replyButton.on('click', function(event) {\n var isTeacher = $('#body').hasClass('teacher_admin');\n if (isTeacher) {\n return true;\n }\n event.preventDefault();\n var url = $(this).attr('href');\n\n var urlParts = url.split('#');\n var baseUrl = urlParts[0];\n var hash = urlParts.length > 1 ? '#' + urlParts[1] : '';\n\n if (baseUrl.indexOf('setformat=') > -1) {\n baseUrl = baseUrl.replace(/setformat=\\d/, 'setformat=1');\n } else if (baseUrl.indexOf('?') > -1) {\n baseUrl += '&setformat=1';\n } else {\n baseUrl += '?setformat=1';\n }\n var finalUrl = baseUrl + hash;\n\n window.location.href = finalUrl;\n });\n }\n\n var ids = $(\"#\" + entry.id).data(\"post-id\");\n var cmid = M.cfg.contextInstanceId;\n\n let args = {id: ids, modulename: \"forum\", cmid: cmid};\n let methodname = 'cursive_get_forum_comment_link';\n let com = AJAX.call([{methodname, args}]);\n com[0].done(function(json) {\n var data = JSON.parse(json);\n\n var filepath = '';\n if (data.data.filename) {\n filepath = data.data.filename;\n }\n if (filepath) {\n\n let analyticButtonDiv = document.createElement('div');\n\n if (!hasApiKey) {\n $(analyticButtonDiv).html(replayButton(ids));\n } else {\n analyticButtonDiv.append(analyticButton(data.data.effort_ratio, ids));\n }\n\n analyticButtonDiv.classList.add('text-center', 'my-2');\n analyticButtonDiv.dataset.region = \"analytic-div\" + ids;\n\n $(\"#\" + entry.id).find('#post-content-' + ids).prepend(analyticButtonDiv);\n\n let myEvents = new AnalyticEvents();\n var context = {\n tabledata: data.data,\n formattime: myEvents.formatedTime(data.data),\n page: scoreSetting,\n userid: ids,\n apikey: hasApiKey\n };\n\n let authIcon = myEvents.authorshipStatus(data.data.first_file, data.data.score, scoreSetting);\n myEvents.createModal(ids, context, '', replayInstances, authIcon);\n myEvents.analytics(ids, templates, context, '', replayInstances, authIcon);\n myEvents.checkDiff(ids, data.data.file_id, '', replayInstances);\n myEvents.replyWriting(ids, filepath, '', replayInstances);\n }\n\n });\n return com.usercomment;\n });\n },\n };\n return usersTable;\n\n\n});"],"names":["define","$","AJAX","str","templates","Replay","analyticButton","replayButton","AnalyticEvents","replayInstances","window","video_playback","mid","filepath","replay","render","then","html","catch","e","console","error","usersTable","init","scoreSetting","showcomment","hasApiKey","get_strings","key","component","done","getToken","find","get","forEach","entry","replyButton","length","on","event","hasClass","preventDefault","urlParts","this","attr","split","baseUrl","hash","indexOf","replace","finalUrl","location","href","ids","id","data","cmid","M","cfg","contextInstanceId","args","modulename","com","call","methodname","json","JSON","parse","filename","analyticButtonDiv","document","createElement","append","effort_ratio","classList","add","dataset","region","prepend","myEvents","context","tabledata","formattime","formatedTime","page","userid","apikey","authIcon","authorshipStatus","first_file","score","createModal","analytics","checkDiff","file_id","replyWriting","usercomment"],"mappings":"AAsBAA,wCAAO,CAAC,SAAU,YAAa,WAAY,iBAAkB,WAAY,oBACrE,kBAAmB,sBAAsB,SACzCC,EACAC,KACAC,IACAC,UACAC,OACAC,eACAC,aACAC,sBAEMC,gBAAkB,GAExBC,OAAOC,eAAiB,SAASC,IAAKC,aACjB,KAAbA,SAAiB,OACXC,OAAS,IAAIT,OACf,UAAYO,IACZC,SACA,IACA,EACA,UAAYD,KAEhBH,gBAAgBG,KAAOE,YAEvBV,UAAUW,OAAO,8BAA8BC,MAAKC,OAChDhB,EAAE,WAAaW,KAAKK,KAAKA,OAClB,KACRC,OAAMC,GAAKT,OAAOU,QAAQC,MAAMF,YAEhC,OAIPG,WAAa,CACbC,KAAM,SAASC,aAAcC,YAAaC,WACtCvB,IACKwB,YAAY,CACT,CAACC,IAAK,gBAAiBC,UAAW,kBAErCC,MAAK,WACFR,WAAWS,SAASP,aAAcC,YAAaC,eAG3DK,SAAU,SAASP,aAAcC,YAAaC,WAC1CzB,EAAE,2BAA2B+B,KAAK,WAAWC,MAAMC,SAAQ,SAASC,WAC5DC,YAAcnC,EAAE,+CAChBmC,YAAYC,OAAS,GACrBD,YAAYE,GAAG,SAAS,SAASC,UACbtC,EAAE,SAASuC,SAAS,wBAEzB,EAEXD,MAAME,qBAGFC,SAFMzC,EAAE0C,MAAMC,KAAK,QAEJC,MAAM,KACrBC,QAAUJ,SAAS,GACnBK,KAAOL,SAASL,OAAS,EAAI,IAAMK,SAAS,GAAK,GAEjDI,QAAQE,QAAQ,eAAiB,EACjCF,QAAUA,QAAQG,QAAQ,eAAgB,eACnCH,QAAQE,QAAQ,MAAQ,EAC/BF,SAAW,eAEXA,SAAW,mBAEXI,SAAWJ,QAAUC,KAEzBrC,OAAOyC,SAASC,KAAOF,gBAI3BG,IAAMpD,EAAE,IAAMkC,MAAMmB,IAAIC,KAAK,WAC7BC,KAAOC,EAAEC,IAAIC,sBAEbC,KAAO,CAACN,GAAID,IAAKQ,WAAY,QAASL,KAAMA,MAE5CM,IAAM5D,KAAK6D,KAAK,CAAC,CAACC,WADL,iCACiBJ,KAAAA,eAClCE,IAAI,GAAGhC,MAAK,SAASmC,UACbV,KAAOW,KAAKC,MAAMF,MAElBpD,SAAW,MACX0C,KAAKA,KAAKa,WACVvD,SAAW0C,KAAKA,KAAKa,UAErBvD,SAAU,KAENwD,kBAAoBC,SAASC,cAAc,OAE1C7C,UAGD2C,kBAAkBG,OAAOlE,eAAeiD,KAAKA,KAAKkB,aAAcpB,MAFhEpD,EAAEoE,mBAAmBpD,KAAKV,aAAa8C,MAK3CgB,kBAAkBK,UAAUC,IAAI,cAAe,QAC/CN,kBAAkBO,QAAQC,OAAS,eAAiBxB,IAEpDpD,EAAE,IAAMkC,MAAMmB,IAAItB,KAAK,iBAAmBqB,KAAKyB,QAAQT,uBAEnDU,SAAW,IAAIvE,mBACfwE,QAAU,CACVC,UAAW1B,KAAKA,KAChB2B,WAAYH,SAASI,aAAa5B,KAAKA,MACvC6B,KAAM5D,aACN6D,OAAQhC,IACRiC,OAAQ5D,eAGR6D,SAAWR,SAASS,iBAAiBjC,KAAKA,KAAKkC,WAAYlC,KAAKA,KAAKmC,MAAOlE,cAChFuD,SAASY,YAAYtC,IAAK2B,QAAS,GAAIvE,gBAAiB8E,UACxDR,SAASa,UAAUvC,IAAKjD,UAAW4E,QAAS,GAAIvE,gBAAiB8E,UACjER,SAASc,UAAUxC,IAAKE,KAAKA,KAAKuC,QAAS,GAAIrF,iBAC/CsE,SAASgB,aAAa1C,IAAKxC,SAAU,GAAIJ,qBAI1CqD,IAAIkC,wBAIhB1E"}
\ No newline at end of file
diff --git a/amd/build/append_submissions_table.min.js b/amd/build/append_submissions_table.min.js
index 50a9c343..6fa7b33d 100644
--- a/amd/build/append_submissions_table.min.js
+++ b/amd/build/append_submissions_table.min.js
@@ -1,3 +1,3 @@
-define("tiny_cursive/append_submissions_table",["jquery","core/ajax","core/str","core/templates","./replay","./analytic_button","./replay_button","./analytic_events","core/str"],(function($,AJAX,str,templates,Replay,analyticButton,replayButton,AnalyticEvents,Str){const replayInstances={};window.video_playback=function(mid,filepath){if(""!==filepath){const replay=new Replay("content"+mid,filepath,10,!1,"player_"+mid);replayInstances[mid]=replay}else templates.render("tiny_cursive/no_submission").then((html=>($("#content"+mid).html(html),!0))).catch((e=>window.console.error(e)));return!1};var usersTable={init:function(scoreSetting,showcomment,hasApiKey){str.get_strings([{key:"confidence_threshold",component:"tiny_cursive"}]).done((function(){usersTable.appendTable(scoreSetting,hasApiKey)}))},appendTable:function(scoreSetting,hasApiKey){let subUrl=window.location.href,parm=new URL(subUrl),hTr=$("thead").find("tr").get()[0];Str.get_string("analytics","tiny_cursive").then((analyticString=>($(hTr).find("th").eq(3).after(''),$("tbody").find("tr").get().forEach((function(tr){var _$$find,_$$find$get$;let tdUser=$(tr).find("td").get()[0],userid=null===(_$$find=$(tdUser).find("input[type='checkbox']"))||void 0===_$$find||null===(_$$find$get$=_$$find.get()[0])||void 0===_$$find$get$?void 0:_$$find$get$.value,cmid=parm.searchParams.get("id"),args={id:userid,modulename:"assign",cmid:cmid},com=AJAX.call([{methodname:"cursive_user_list_submission_stats",args:args}]);try{com[0].done((function(json){var data=JSON.parse(json),filepath="";data.res.filename&&(filepath=data.res.filename);const tableCell=document.createElement("td");hasApiKey?tableCell.appendChild(analyticButton(data.res.effort_ratio,userid)):$(tableCell).html(replayButton(userid)),$(tr).find("td").eq(3).after(tableCell);let textContent=document.querySelector(".page-header-headings h1").textContent,myEvents=new AnalyticEvents;var context={tabledata:data.res,formattime:myEvents.formatedTime(data.res),moduletitle:textContent,page:scoreSetting,userid:userid,apikey:hasApiKey};let authIcon=myEvents.authorshipStatus(data.res.first_file,data.res.score,scoreSetting);myEvents.createModal(userid,context,"",replayInstances,authIcon),myEvents.analytics(userid,templates,context,"",replayInstances,authIcon),myEvents.checkDiff(userid,data.res.file_id,"",replayInstances),myEvents.replyWriting(userid,filepath,"",replayInstances)})).fail((function(error){window.console.error("AJAX request failed:",error)}))}catch(error){window.console.error("Error processing data:",error)}return com.usercomment})),!0))).catch((error=>{window.console.error("Failed to get analytics string:",error)}))}};return usersTable}));
+define("tiny_cursive/append_submissions_table",["jquery","core/ajax","core/str","core/templates","./replay","./analytic_button","./replay_button","./analytic_events","core/str"],(function($,AJAX,str,templates,Replay,analyticButton,replayButton,AnalyticEvents,Str){const replayInstances={};window.video_playback=function(mid,filepath){if(""!==filepath){const replay=new Replay("content"+mid,filepath,10,!1,"player_"+mid);replayInstances[mid]=replay}else templates.render("tiny_cursive/no_submission").then((html=>($("#content"+mid).html(html),!0))).catch((e=>window.console.error(e)));return!1};var usersTable={init:function(scoreSetting,showcomment,hasApiKey){str.get_strings([{key:"confidence_threshold",component:"tiny_cursive"}]).done((function(){usersTable.appendTable(scoreSetting,hasApiKey)}))},appendTable:function(scoreSetting,hasApiKey){let subUrl=window.location.href,parm=new URL(subUrl),hTr=$("table#submissions thead").find("tr").get()[0];Str.get_string("analytics","tiny_cursive").then((analyticString=>($(hTr).find("th").eq(3).after(''),$("table#submissions tbody").find("tr").get().forEach((function(tr){var _$$find,_$$find$get$;let tdUser=$(tr).find("td").get()[0],userid=null===(_$$find=$(tdUser).find("input[type='checkbox']"))||void 0===_$$find||null===(_$$find$get$=_$$find.get()[0])||void 0===_$$find$get$?void 0:_$$find$get$.value,cmid=parm.searchParams.get("id"),args={id:userid,modulename:"assign",cmid:cmid},com=AJAX.call([{methodname:"cursive_user_list_submission_stats",args:args}]);try{com[0].done((function(json){var data=JSON.parse(json),filepath="";data.res.filename&&(filepath=data.res.filename);const tableCell=document.createElement("td");hasApiKey?tableCell.appendChild(analyticButton(data.res.effort_ratio,userid)):$(tableCell).html(replayButton(userid)),$(tr).find("td").eq(3).after(tableCell);let textContent=document.querySelector(".page-header-headings h1").textContent,myEvents=new AnalyticEvents;var context={tabledata:data.res,formattime:myEvents.formatedTime(data.res),moduletitle:textContent,page:scoreSetting,userid:userid,apikey:hasApiKey};let authIcon=myEvents.authorshipStatus(data.res.first_file,data.res.score,scoreSetting);myEvents.createModal(userid,context,"",replayInstances,authIcon),myEvents.analytics(userid,templates,context,"",replayInstances,authIcon),myEvents.checkDiff(userid,data.res.file_id,"",replayInstances),myEvents.replyWriting(userid,filepath,"",replayInstances)})).fail((function(error){window.console.error("AJAX request failed:",error)}))}catch(error){window.console.error("Error processing data:",error)}return com.usercomment})),!0))).catch((error=>{window.console.error("Failed to get analytics string:",error)}))}};return usersTable}));
//# sourceMappingURL=append_submissions_table.min.js.map
\ No newline at end of file
diff --git a/amd/build/append_submissions_table.min.js.map b/amd/build/append_submissions_table.min.js.map
index 0ee320af..cbdf1a12 100644
--- a/amd/build/append_submissions_table.min.js.map
+++ b/amd/build/append_submissions_table.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"append_submissions_table.min.js","sources":["../src/append_submissions_table.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * @module tiny_cursive/append_submissions_table\n * @category TinyMCE Editor\n * @copyright CTI \n * @author kuldeep singh \n */\n\ndefine([\n \"jquery\",\n \"core/ajax\",\n \"core/str\",\n \"core/templates\",\n \"./replay\",\n './analytic_button',\n './replay_button',\n './analytic_events',\n 'core/str'], function(\n $,\n AJAX,\n str,\n templates,\n Replay,\n analyticButton,\n replayButton,\n AnalyticEvents,\n Str\n) {\n const replayInstances = {};\n // eslint-disable-next-line camelcase\n window.video_playback = function(mid, filepath) {\n\n if (filepath !== '') {\n const replay = new Replay(\n 'content' + mid,\n filepath,\n 10,\n false,\n 'player_' + mid\n );\n replayInstances[mid] = replay;\n } else {\n templates.render('tiny_cursive/no_submission').then(html => {\n $('#content' + mid).html(html);\n return true;\n }).catch(e => window.console.error(e));\n }\n return false;\n\n };\n\n var usersTable = {\n init: function(scoreSetting, showcomment, hasApiKey) {\n str\n .get_strings([\n {key: \"confidence_threshold\", component: \"tiny_cursive\"},\n ]).done(function() {\n usersTable.appendTable(scoreSetting, hasApiKey);\n });\n },\n appendTable: function(scoreSetting, hasApiKey) {\n let subUrl = window.location.href;\n let parm = new URL(subUrl);\n let hTr = $('thead').find('tr').get()[0];\n\n Str.get_string('analytics', 'tiny_cursive')\n .then(analyticString => {\n $(hTr).find('th').eq(3).after('');\n $('tbody').find(\"tr\").get().forEach(function(tr) {\n let tdUser = $(tr).find(\"td\").get()[0];\n let userid = $(tdUser).find(\"input[type='checkbox']\")?.get()[0]?.value;\n let cmid = parm.searchParams.get('id');\n // Create the table cell element and append the anchor.\n\n let args = {id: userid, modulename: \"assign\", cmid: cmid};\n let methodname = 'cursive_user_list_submission_stats';\n let com = AJAX.call([{methodname, args}]);\n try {\n com[0].done(function(json) {\n var data = JSON.parse(json);\n var filepath = '';\n if (data.res.filename) {\n filepath = data.res.filename;\n }\n\n const tableCell = document.createElement('td');\n\n if (!hasApiKey) {\n $(tableCell).html(replayButton(userid));\n } else {\n tableCell.appendChild(analyticButton(data.res.effort_ratio, userid));\n }\n $(tr).find('td').eq(3).after(tableCell);\n\n // Get Module Name from element.\n let element = document.querySelector('.page-header-headings h1');\n // Selects the h1 element within the .page-header-headings class\n let textContent = element.textContent; // Extracts the text content from the h1 element\n\n let myEvents = new AnalyticEvents();\n var context = {\n tabledata: data.res,\n formattime: myEvents.formatedTime(data.res),\n moduletitle: textContent,\n page: scoreSetting,\n userid: userid,\n apikey: hasApiKey\n };\n\n let authIcon = myEvents.authorshipStatus(data.res.first_file, data.res.score, scoreSetting);\n myEvents.createModal(userid, context, '', replayInstances, authIcon);\n myEvents.analytics(userid, templates, context, '', replayInstances, authIcon);\n myEvents.checkDiff(userid, data.res.file_id, '', replayInstances);\n myEvents.replyWriting(userid, filepath, '', replayInstances);\n\n }).fail(function(error) {\n window.console.error('AJAX request failed:', error);\n });\n } catch (error) {\n window.console.error('Error processing data:', error);\n }\n return com.usercomment;\n });\n return true;\n })\n .catch(error => {\n window.console.error('Failed to get analytics string:', error);\n });\n }\n };\n\n return usersTable;\n});"],"names":["define","$","AJAX","str","templates","Replay","analyticButton","replayButton","AnalyticEvents","Str","replayInstances","window","video_playback","mid","filepath","replay","render","then","html","catch","e","console","error","usersTable","init","scoreSetting","showcomment","hasApiKey","get_strings","key","component","done","appendTable","subUrl","location","href","parm","URL","hTr","find","get","get_string","analyticString","eq","after","forEach","tr","tdUser","userid","_$$find","_$$find$get$","value","cmid","searchParams","args","id","modulename","com","call","methodname","json","data","JSON","parse","res","filename","tableCell","document","createElement","appendChild","effort_ratio","textContent","querySelector","myEvents","context","tabledata","formattime","formatedTime","moduletitle","page","apikey","authIcon","authorshipStatus","first_file","score","createModal","analytics","checkDiff","file_id","replyWriting","fail","usercomment"],"mappings":"AAsBAA,+CAAO,CACH,SACA,YACA,WACA,iBACA,WACA,oBACA,kBACA,oBACA,aAAa,SACbC,EACAC,KACAC,IACAC,UACAC,OACAC,eACAC,aACAC,eACAC,WAEMC,gBAAkB,GAExBC,OAAOC,eAAiB,SAASC,IAAKC,aAEjB,KAAbA,SAAiB,OACXC,OAAS,IAAIV,OACf,UAAYQ,IACZC,SACA,IACA,EACA,UAAYD,KAEhBH,gBAAgBG,KAAOE,YAEvBX,UAAUY,OAAO,8BAA8BC,MAAKC,OAChDjB,EAAE,WAAaY,KAAKK,KAAKA,OAClB,KACRC,OAAMC,GAAKT,OAAOU,QAAQC,MAAMF,YAEhC,OAIPG,WAAa,CACbC,KAAM,SAASC,aAAcC,YAAaC,WACtCxB,IACKyB,YAAY,CACT,CAACC,IAAK,uBAAwBC,UAAW,kBAC1CC,MAAK,WACRR,WAAWS,YAAYP,aAAcE,eAG7CK,YAAa,SAASP,aAAcE,eAC5BM,OAAStB,OAAOuB,SAASC,KACzBC,KAAO,IAAIC,IAAIJ,QACfK,IAAMrC,EAAE,SAASsC,KAAK,MAAMC,MAAM,GAEtC/B,IAAIgC,WAAW,YAAa,gBACvBxB,MAAKyB,iBACFzC,EAAEqC,KAAKC,KAAK,MAAMI,GAAG,GAAGC,MAAM,qCACxBF,eADwB,+FAG9BzC,EAAE,SAASsC,KAAK,MAAMC,MAAMK,SAAQ,SAASC,iCACrCC,OAAS9C,EAAE6C,IAAIP,KAAK,MAAMC,MAAM,GAChCQ,uBAAS/C,EAAE8C,QAAQR,KAAK,mEAAfU,QAA0CT,MAAM,kCAAhDU,aAAoDC,MAC7DC,KAAOhB,KAAKiB,aAAab,IAAI,MAG7Bc,KAAO,CAACC,GAAIP,OAAQQ,WAAY,SAAUJ,KAAMA,MAEhDK,IAAMvD,KAAKwD,KAAK,CAAC,CAACC,WADL,qCACiBL,KAAAA,YAE9BG,IAAI,GAAG1B,MAAK,SAAS6B,UACbC,KAAOC,KAAKC,MAAMH,MAClB9C,SAAW,GACX+C,KAAKG,IAAIC,WACTnD,SAAW+C,KAAKG,IAAIC,gBAGlBC,UAAYC,SAASC,cAAc,MAEpCzC,UAGDuC,UAAUG,YAAY/D,eAAeuD,KAAKG,IAAIM,aAActB,SAF5D/C,EAAEiE,WAAWhD,KAAKX,aAAayC,SAInC/C,EAAE6C,IAAIP,KAAK,MAAMI,GAAG,GAAGC,MAAMsB,eAKzBK,YAFUJ,SAASK,cAAc,4BAEXD,YAEtBE,SAAW,IAAIjE,mBACfkE,QAAU,CACVC,UAAWd,KAAKG,IAChBY,WAAYH,SAASI,aAAahB,KAAKG,KACvCc,YAAaP,YACbQ,KAAMtD,aACNuB,OAAQA,OACRgC,OAAQrD,eAGRsD,SAAWR,SAASS,iBAAiBrB,KAAKG,IAAImB,WAAYtB,KAAKG,IAAIoB,MAAO3D,cAC9EgD,SAASY,YAAYrC,OAAQ0B,QAAS,GAAIhE,gBAAiBuE,UAC3DR,SAASa,UAAUtC,OAAQ5C,UAAWsE,QAAS,GAAIhE,gBAAiBuE,UACpER,SAASc,UAAUvC,OAAQa,KAAKG,IAAIwB,QAAS,GAAI9E,iBACjD+D,SAASgB,aAAazC,OAAQlC,SAAU,GAAIJ,oBAE7CgF,MAAK,SAASpE,OACbX,OAAOU,QAAQC,MAAM,uBAAwBA,UAEnD,MAAOA,OACLX,OAAOU,QAAQC,MAAM,yBAA0BA,cAE5CmC,IAAIkC,gBAER,KAEVxE,OAAMG,QACHX,OAAOU,QAAQC,MAAM,kCAAmCA,mBAKjEC"}
\ No newline at end of file
+{"version":3,"file":"append_submissions_table.min.js","sources":["../src/append_submissions_table.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * @module tiny_cursive/append_submissions_table\n * @category TinyMCE Editor\n * @copyright CTI \n * @author kuldeep singh \n */\n\ndefine([\n \"jquery\",\n \"core/ajax\",\n \"core/str\",\n \"core/templates\",\n \"./replay\",\n './analytic_button',\n './replay_button',\n './analytic_events',\n 'core/str'], function(\n $,\n AJAX,\n str,\n templates,\n Replay,\n analyticButton,\n replayButton,\n AnalyticEvents,\n Str\n) {\n const replayInstances = {};\n // eslint-disable-next-line camelcase\n window.video_playback = function(mid, filepath) {\n\n if (filepath !== '') {\n const replay = new Replay(\n 'content' + mid,\n filepath,\n 10,\n false,\n 'player_' + mid\n );\n replayInstances[mid] = replay;\n } else {\n templates.render('tiny_cursive/no_submission').then(html => {\n $('#content' + mid).html(html);\n return true;\n }).catch(e => window.console.error(e));\n }\n return false;\n\n };\n\n var usersTable = {\n init: function(scoreSetting, showcomment, hasApiKey) {\n str\n .get_strings([\n {key: \"confidence_threshold\", component: \"tiny_cursive\"},\n ]).done(function() {\n usersTable.appendTable(scoreSetting, hasApiKey);\n });\n },\n appendTable: function(scoreSetting, hasApiKey) {\n let subUrl = window.location.href;\n let parm = new URL(subUrl);\n let hTr = $('table#submissions thead').find('tr').get()[0];\n\n Str.get_string('analytics', 'tiny_cursive')\n .then(analyticString => {\n $(hTr).find('th').eq(3).after('');\n $('table#submissions tbody').find(\"tr\").get().forEach(function(tr) {\n let tdUser = $(tr).find(\"td\").get()[0];\n let userid = $(tdUser).find(\"input[type='checkbox']\")?.get()[0]?.value;\n let cmid = parm.searchParams.get('id');\n // Create the table cell element and append the anchor.\n\n let args = {id: userid, modulename: \"assign\", cmid: cmid};\n let methodname = 'cursive_user_list_submission_stats';\n let com = AJAX.call([{methodname, args}]);\n try {\n com[0].done(function(json) {\n var data = JSON.parse(json);\n var filepath = '';\n if (data.res.filename) {\n filepath = data.res.filename;\n }\n\n const tableCell = document.createElement('td');\n\n if (!hasApiKey) {\n $(tableCell).html(replayButton(userid));\n } else {\n tableCell.appendChild(analyticButton(data.res.effort_ratio, userid));\n }\n $(tr).find('td').eq(3).after(tableCell);\n\n // Get Module Name from element.\n let element = document.querySelector('.page-header-headings h1');\n // Selects the h1 element within the .page-header-headings class\n let textContent = element.textContent; // Extracts the text content from the h1 element\n\n let myEvents = new AnalyticEvents();\n var context = {\n tabledata: data.res,\n formattime: myEvents.formatedTime(data.res),\n moduletitle: textContent,\n page: scoreSetting,\n userid: userid,\n apikey: hasApiKey\n };\n\n let authIcon = myEvents.authorshipStatus(data.res.first_file, data.res.score, scoreSetting);\n myEvents.createModal(userid, context, '', replayInstances, authIcon);\n myEvents.analytics(userid, templates, context, '', replayInstances, authIcon);\n myEvents.checkDiff(userid, data.res.file_id, '', replayInstances);\n myEvents.replyWriting(userid, filepath, '', replayInstances);\n\n }).fail(function(error) {\n window.console.error('AJAX request failed:', error);\n });\n } catch (error) {\n window.console.error('Error processing data:', error);\n }\n return com.usercomment;\n });\n return true;\n })\n .catch(error => {\n window.console.error('Failed to get analytics string:', error);\n });\n }\n };\n\n return usersTable;\n});"],"names":["define","$","AJAX","str","templates","Replay","analyticButton","replayButton","AnalyticEvents","Str","replayInstances","window","video_playback","mid","filepath","replay","render","then","html","catch","e","console","error","usersTable","init","scoreSetting","showcomment","hasApiKey","get_strings","key","component","done","appendTable","subUrl","location","href","parm","URL","hTr","find","get","get_string","analyticString","eq","after","forEach","tr","tdUser","userid","_$$find","_$$find$get$","value","cmid","searchParams","args","id","modulename","com","call","methodname","json","data","JSON","parse","res","filename","tableCell","document","createElement","appendChild","effort_ratio","textContent","querySelector","myEvents","context","tabledata","formattime","formatedTime","moduletitle","page","apikey","authIcon","authorshipStatus","first_file","score","createModal","analytics","checkDiff","file_id","replyWriting","fail","usercomment"],"mappings":"AAsBAA,+CAAO,CACH,SACA,YACA,WACA,iBACA,WACA,oBACA,kBACA,oBACA,aAAa,SACbC,EACAC,KACAC,IACAC,UACAC,OACAC,eACAC,aACAC,eACAC,WAEMC,gBAAkB,GAExBC,OAAOC,eAAiB,SAASC,IAAKC,aAEjB,KAAbA,SAAiB,OACXC,OAAS,IAAIV,OACf,UAAYQ,IACZC,SACA,IACA,EACA,UAAYD,KAEhBH,gBAAgBG,KAAOE,YAEvBX,UAAUY,OAAO,8BAA8BC,MAAKC,OAChDjB,EAAE,WAAaY,KAAKK,KAAKA,OAClB,KACRC,OAAMC,GAAKT,OAAOU,QAAQC,MAAMF,YAEhC,OAIPG,WAAa,CACbC,KAAM,SAASC,aAAcC,YAAaC,WACtCxB,IACKyB,YAAY,CACT,CAACC,IAAK,uBAAwBC,UAAW,kBAC1CC,MAAK,WACRR,WAAWS,YAAYP,aAAcE,eAG7CK,YAAa,SAASP,aAAcE,eAC5BM,OAAStB,OAAOuB,SAASC,KACzBC,KAAO,IAAIC,IAAIJ,QACfK,IAAMrC,EAAE,2BAA2BsC,KAAK,MAAMC,MAAM,GAExD/B,IAAIgC,WAAW,YAAa,gBACvBxB,MAAKyB,iBACFzC,EAAEqC,KAAKC,KAAK,MAAMI,GAAG,GAAGC,MAAM,qCACxBF,eADwB,+FAG9BzC,EAAE,2BAA2BsC,KAAK,MAAMC,MAAMK,SAAQ,SAASC,iCACvDC,OAAS9C,EAAE6C,IAAIP,KAAK,MAAMC,MAAM,GAChCQ,uBAAS/C,EAAE8C,QAAQR,KAAK,mEAAfU,QAA0CT,MAAM,kCAAhDU,aAAoDC,MAC7DC,KAAOhB,KAAKiB,aAAab,IAAI,MAG7Bc,KAAO,CAACC,GAAIP,OAAQQ,WAAY,SAAUJ,KAAMA,MAEhDK,IAAMvD,KAAKwD,KAAK,CAAC,CAACC,WADL,qCACiBL,KAAAA,YAE9BG,IAAI,GAAG1B,MAAK,SAAS6B,UACbC,KAAOC,KAAKC,MAAMH,MAClB9C,SAAW,GACX+C,KAAKG,IAAIC,WACTnD,SAAW+C,KAAKG,IAAIC,gBAGlBC,UAAYC,SAASC,cAAc,MAEpCzC,UAGDuC,UAAUG,YAAY/D,eAAeuD,KAAKG,IAAIM,aAActB,SAF5D/C,EAAEiE,WAAWhD,KAAKX,aAAayC,SAInC/C,EAAE6C,IAAIP,KAAK,MAAMI,GAAG,GAAGC,MAAMsB,eAKzBK,YAFUJ,SAASK,cAAc,4BAEXD,YAEtBE,SAAW,IAAIjE,mBACfkE,QAAU,CACVC,UAAWd,KAAKG,IAChBY,WAAYH,SAASI,aAAahB,KAAKG,KACvCc,YAAaP,YACbQ,KAAMtD,aACNuB,OAAQA,OACRgC,OAAQrD,eAGRsD,SAAWR,SAASS,iBAAiBrB,KAAKG,IAAImB,WAAYtB,KAAKG,IAAIoB,MAAO3D,cAC9EgD,SAASY,YAAYrC,OAAQ0B,QAAS,GAAIhE,gBAAiBuE,UAC3DR,SAASa,UAAUtC,OAAQ5C,UAAWsE,QAAS,GAAIhE,gBAAiBuE,UACpER,SAASc,UAAUvC,OAAQa,KAAKG,IAAIwB,QAAS,GAAI9E,iBACjD+D,SAASgB,aAAazC,OAAQlC,SAAU,GAAIJ,oBAE7CgF,MAAK,SAASpE,OACbX,OAAOU,QAAQC,MAAM,uBAAwBA,UAEnD,MAAOA,OACLX,OAAOU,QAAQC,MAAM,yBAA0BA,cAE5CmC,IAAIkC,gBAER,KAEVxE,OAAMG,QACHX,OAAOU,QAAQC,MAAM,kCAAmCA,mBAKjEC"}
\ No newline at end of file
diff --git a/amd/build/scatter_chart.min.js b/amd/build/scatter_chart.min.js
index 4dcd1cc7..950f6fde 100644
--- a/amd/build/scatter_chart.min.js
+++ b/amd/build/scatter_chart.min.js
@@ -6,6 +6,6 @@ define("tiny_cursive/scatter_chart",["exports","core/chartjs","core/str"],(funct
* @module tiny_cursive/scatter_chart
* @copyright 2025 Cursive Technology, Inc.
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_chartjs=(obj=_chartjs)&&obj.__esModule?obj:{default:obj};_exports.init=async(data,apiKey,caption)=>{const ctx=document.getElementById("effortScatterChart").getContext("2d");data&&(data=JSON.parse(document.getElementById("scatter-chart-data").dataset.data));let display=!0,isEmpty="";var dataset=[];const[applyFilter,noSubmission,noPayload,freemium]=await(0,_str.get_strings)([{key:"apply_filter",component:"tiny_cursive"},{key:"no_submission",component:"tiny_cursive"},{key:"nopaylod",component:"tiny_cursive"},{key:"freemium",component:"tiny_cursive"},{key:"chart_result",component:"tiny_cursive"}]);Array.isArray(data)&&!data.state&&apiKey&&(dataset=data,isEmpty=data.some((ds=>Array.isArray(ds.data)&&ds.data.some((point=>point&&"object"==typeof point&&Object.keys(point).length>0))))),apiKey&&0!==data.length&&isEmpty&&!1!==data||(display=!1);const fallbackMessagePlugin={id:"fallbackMessagePlugin",afterDraw(chart){apiKey?"apply_filter"!=data.state?"no_submission"!==data.state?isEmpty||data.state||drawMessage("⚠ "+noPayload,chart):drawMessage("⚠ "+noSubmission,chart):drawMessage("⚠ "+applyFilter,chart):drawMessage("⚠ "+freemium,chart)}};function formatTime(value){const minutes=Math.floor(value/60),seconds=value%60;return`${String(minutes).padStart(2,"0")}:${String(seconds).padStart(2,"0")}`}function drawMessage(text,chart){const{ctx:ctx,chartArea:{left:left,right:right,top:top,bottom:bottom}}=chart;ctx.save(),ctx.textAlign="center",ctx.textBaseline="middle",ctx.font='bold 16px "Segoe UI", Arial',ctx.fillStyle="#666";const centerX=(left+right)/2,centerY=(top+bottom)/2;ctx.fillText(text,centerX,centerY),ctx.restore()}new _chartjs.default(ctx,{type:"scatter",data:{datasets:dataset},options:{plugins:{title:{display:display,text:caption,font:{size:16,weight:"bold"},color:"#333",padding:{top:10,bottom:20},align:"center"},legend:{display:!0,position:"bottom",labels:{usePointStyle:!0,pointStyle:"circle",padding:20}},tooltip:{backgroundColor:"rgba(252, 252, 252, 0.8)",titleColor:"#000",bodyColor:"#000",borderColor:"#cccccc",borderWidth:1,displayColors:!1,callbacks:{title:function(context){return context[0].raw.label},label:function(context){const d=context.raw;return[`Time: ${formatTime(d.x)}`,`Effort: ${Math.round(100*d.effort*100)/100}%`,`Words: ${d.words}`,`WPM: ${d.wpm}`]}}}},scales:{x:{title:{display:!0,text:"Time Spent (mm:ss)"},min:0,ticks:{callback:function(value){return formatTime(value)}}},y:{title:{display:!0,text:"Effort Score"},min:0,ticks:{stepSize:.5}}}},plugins:[fallbackMessagePlugin]})}}));
+ */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_chartjs=(obj=_chartjs)&&obj.__esModule?obj:{default:obj};_exports.init=async(data,apiKey,caption)=>{const ctx=document.getElementById("effortScatterChart").getContext("2d");data&&(data=JSON.parse(document.getElementById("scatter-chart-data").dataset.data));let display=!0,isEmpty="";var dataset=[];const[applyFilter,noSubmission,noPayload,freemium,time,words,effortratio,wpm,effortscore,timespent]=await(0,_str.get_strings)([{key:"apply_filter",component:"tiny_cursive"},{key:"no_submission",component:"tiny_cursive"},{key:"nopaylod",component:"tiny_cursive"},{key:"freemium",component:"tiny_cursive"},{key:"time",component:"tiny_cursive"},{key:"words",component:"tiny_cursive"},{key:"effort_ratio",component:"tiny_cursive"},{key:"wpm",component:"tiny_cursive"},{key:"effort_score",component:"tiny_cursive"},{key:"timespent",component:"tiny_cursive"}]);Array.isArray(data)&&!data.state&&apiKey&&(dataset=data,isEmpty=data.some((ds=>Array.isArray(ds.data)&&ds.data.some((point=>point&&"object"==typeof point&&Object.keys(point).length>0))))),apiKey&&0!==data.length&&isEmpty&&!1!==data||(display=!1);const fallbackMessagePlugin={id:"fallbackMessagePlugin",afterDraw(chart){apiKey?"apply_filter"!=data.state?"no_submission"!==data.state?isEmpty||data.state||drawMessage("⚠ "+noPayload,chart):drawMessage("⚠ "+noSubmission,chart):drawMessage("⚠ "+applyFilter,chart):drawMessage("⚠ "+freemium,chart)}};function formatTime(value){const minutes=Math.floor(value/60),seconds=value%60;return`${String(minutes).padStart(2,"0")}:${String(seconds).padStart(2,"0")}`}function drawMessage(text,chart){const{ctx:ctx,chartArea:{left:left,right:right,top:top,bottom:bottom}}=chart;ctx.save(),ctx.textAlign="center",ctx.textBaseline="middle",ctx.font='bold 16px "Segoe UI", Arial',ctx.fillStyle="#666";const centerX=(left+right)/2,centerY=(top+bottom)/2;ctx.fillText(text,centerX,centerY),ctx.restore()}new _chartjs.default(ctx,{type:"scatter",data:{datasets:dataset},options:{plugins:{title:{display:display,text:caption,font:{size:16,weight:"bold"},color:"#333",padding:{top:10,bottom:20},align:"center"},legend:{display:!0,position:"bottom",labels:{usePointStyle:!0,pointStyle:"circle",padding:20}},tooltip:{backgroundColor:"rgba(252, 252, 252, 0.8)",titleColor:"#000",bodyColor:"#000",borderColor:"#cccccc",borderWidth:1,displayColors:!1,callbacks:{title:function(context){return context[0].raw.label},label:function(context){const d=context.raw;return[`${time}: ${formatTime(d.x)}`,`${effortratio}: ${Math.round(100*d.effort*100)/100}%`,`${words}: ${d.words}`,`${wpm}: ${d.wpm}`]}}}},scales:{x:{title:{display:!0,text:timespent},min:0,ticks:{callback:function(value){return formatTime(value)}}},y:{title:{display:!0,text:effortscore},min:0,ticks:{stepSize:.5}}}},plugins:[fallbackMessagePlugin]})}}));
//# sourceMappingURL=scatter_chart.min.js.map
\ No newline at end of file
diff --git a/amd/build/scatter_chart.min.js.map b/amd/build/scatter_chart.min.js.map
index 8b434228..28cfaef5 100644
--- a/amd/build/scatter_chart.min.js.map
+++ b/amd/build/scatter_chart.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"scatter_chart.min.js","sources":["../src/scatter_chart.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * A module that creates a scatter chart to visualize student effort data using Chart.js.\n * The chart displays effort scores against time spent, with tooltips showing additional metrics.\n *\n * @module tiny_cursive/scatter_chart\n * @copyright 2025 Cursive Technology, Inc. \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Chart from 'core/chartjs';\nimport {get_strings as getStrings} from 'core/str';\nexport const init = async(data, apiKey, caption) => {\n\n const ctx = document.getElementById('effortScatterChart').getContext('2d');\n if (data) {\n data = JSON.parse(document.getElementById('scatter-chart-data').dataset.data);\n }\n\n let display = true;\n let isEmpty = \"\";\n var dataset = [];\n\n const [\n applyFilter,\n noSubmission,\n noPayload,\n freemium,\n ] = await getStrings([\n {key: 'apply_filter', component: 'tiny_cursive'},\n {key: 'no_submission', component: 'tiny_cursive'},\n {key: 'nopaylod', component: 'tiny_cursive'},\n {key: 'freemium', component: 'tiny_cursive'},\n {key: 'chart_result', component: 'tiny_cursive'}\n ]);\n\n if (Array.isArray(data) && !data.state && apiKey) {\n dataset = data;\n isEmpty = data.some(ds =>\n Array.isArray(ds.data) &&\n ds.data.some(point =>\n point && typeof point === 'object' && Object.keys(point).length > 0\n )\n );\n }\n\n if (!apiKey || data.length === 0 || !isEmpty || data === false) {\n display = false;\n }\n\n const fallbackMessagePlugin = {\n id: 'fallbackMessagePlugin',\n afterDraw(chart) {\n // ⚠ Case 1: Freemium user\n if (!apiKey) {\n drawMessage('⚠ ' + freemium, chart);\n return;\n }\n // ⚠ Case 2: Apply filter (data is empty array)\n if (data.state == \"apply_filter\") {\n drawMessage('⚠ ' + applyFilter, chart);\n return;\n }\n if (data.state === \"no_submission\") {\n drawMessage('⚠ ' + noSubmission, chart);\n return;\n }\n // ⚠ Case 3: No payload data (all `data` arrays are empty or full of empty objects)\n if (!isEmpty && !data.state) {\n drawMessage('⚠ ' + noPayload, chart);\n }\n\n }\n };\n\n new Chart(ctx, {\n type: 'scatter',\n data: {\n datasets: dataset,\n },\n options: {\n plugins: {\n title: {\n display: display,\n text: caption,\n font: {\n size: 16,\n weight: 'bold',\n },\n color: '#333',\n padding: {\n top: 10,\n bottom: 20\n },\n align: 'center'\n },\n legend: {\n display: true,\n position: 'bottom',\n labels: {\n usePointStyle: true,\n pointStyle: 'circle',\n padding: 20\n }\n },\n tooltip: {\n backgroundColor: 'rgba(252, 252, 252, 0.8)',\n titleColor: '#000',\n bodyColor: '#000',\n borderColor: '#cccccc',\n borderWidth: 1,\n displayColors: false,\n callbacks: {\n title: function(context) {\n const d = context[0].raw;\n return d.label; // This appears as bold title.\n },\n label: function(context) {\n const d = context.raw;\n return [\n `Time: ${formatTime(d.x)}`,\n `Effort: ${Math.round(d.effort * 100 * 100) / 100}%`,\n `Words: ${d.words}`,\n `WPM: ${d.wpm}`\n ];\n }\n }\n }\n },\n scales: {\n x: {\n title: {\n display: true,\n text: 'Time Spent (mm:ss)'\n },\n min: 0,\n ticks: {\n callback: function(value) {\n return formatTime(value);\n }\n }\n },\n y: {\n title: {\n display: true,\n text: 'Effort Score'\n },\n min: 0,\n ticks: {\n stepSize: 0.5\n }\n }\n }\n },\n plugins: [fallbackMessagePlugin]\n });\n\n /**\n * Formats a time value in seconds to a mm:ss string format\n * @param {number} value - The time value in seconds\n * @returns {string} The formatted time string in mm:ss format\n */\n function formatTime(value) {\n const minutes = Math.floor(value / 60);\n const seconds = value % 60;\n return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;\n }\n\n /**\n * Draws a message on the chart canvas\n * @param {string} text - The message to be displayed\n * @param {Chart} chart - The Chart.js chart object\n */\n function drawMessage(text, chart) {\n\n const {ctx, chartArea: {left, right, top, bottom}} = chart;\n ctx.save();\n ctx.textAlign = 'center';\n ctx.textBaseline = 'middle';\n ctx.font = 'bold 16px \"Segoe UI\", Arial';\n ctx.fillStyle = '#666';\n\n const centerX = (left + right) / 2;\n const centerY = (top + bottom) / 2;\n\n ctx.fillText(text, centerX, centerY);\n ctx.restore();\n }\n};"],"names":["async","data","apiKey","caption","ctx","document","getElementById","getContext","JSON","parse","dataset","display","isEmpty","applyFilter","noSubmission","noPayload","freemium","key","component","Array","isArray","state","some","ds","point","Object","keys","length","fallbackMessagePlugin","id","afterDraw","chart","drawMessage","formatTime","value","minutes","Math","floor","seconds","String","padStart","text","chartArea","left","right","top","bottom","save","textAlign","textBaseline","font","fillStyle","centerX","centerY","fillText","restore","Chart","type","datasets","options","plugins","title","size","weight","color","padding","align","legend","position","labels","usePointStyle","pointStyle","tooltip","backgroundColor","titleColor","bodyColor","borderColor","borderWidth","displayColors","callbacks","context","raw","label","d","x","round","effort","words","wpm","scales","min","ticks","callback","y","stepSize"],"mappings":";;;;;;;;0JA0BoBA,MAAMC,KAAMC,OAAQC,iBAE9BC,IAAMC,SAASC,eAAe,sBAAsBC,WAAW,MACjEN,OACAA,KAAOO,KAAKC,MAAMJ,SAASC,eAAe,sBAAsBI,QAAQT,WAGxEU,SAAU,EACVC,QAAU,OACVF,QAAU,SAGVG,YACAC,aACAC,UACAC,gBACM,oBAAW,CACjB,CAACC,IAAK,eAAgBC,UAAW,gBACjC,CAACD,IAAK,gBAAiBC,UAAW,gBAClC,CAACD,IAAK,WAAYC,UAAW,gBAC7B,CAACD,IAAK,WAAYC,UAAW,gBAC7B,CAACD,IAAK,eAAgBC,UAAW,kBAGjCC,MAAMC,QAAQnB,QAAUA,KAAKoB,OAASnB,SACtCQ,QAAUT,KACVW,QAAUX,KAAKqB,MAAKC,IAChBJ,MAAMC,QAAQG,GAAGtB,OACjBsB,GAAGtB,KAAKqB,MAAKE,OACTA,OAA0B,iBAAVA,OAAsBC,OAAOC,KAAKF,OAAOG,OAAS,OAKzEzB,QAA0B,IAAhBD,KAAK0B,QAAiBf,UAAoB,IAATX,OAC5CU,SAAU,SAGRiB,sBAAwB,CAC1BC,GAAI,wBACJC,UAAUC,OAED7B,OAKa,gBAAdD,KAAKoB,MAIU,kBAAfpB,KAAKoB,MAKJT,SAAYX,KAAKoB,OAClBW,YAAY,KAAOjB,UAAWgB,OAL9BC,YAAY,KAAOlB,aAAciB,OAJjCC,YAAY,KAAOnB,YAAakB,OALhCC,YAAY,KAAOhB,SAAUe,kBA2GhCE,WAAWC,aACVC,QAAUC,KAAKC,MAAMH,MAAQ,IAC7BI,QAAUJ,MAAQ,SAChB,GAAEK,OAAOJ,SAASK,SAAS,EAAG,QAAQD,OAAOD,SAASE,SAAS,EAAG,gBAQrER,YAAYS,KAAMV,aAEjB3B,IAACA,IAAKsC,WAAWC,KAACA,KAADC,MAAOA,MAAPC,IAAcA,IAAdC,OAAmBA,SAAWf,MACrD3B,IAAI2C,OACJ3C,IAAI4C,UAAY,SAChB5C,IAAI6C,aAAe,SACnB7C,IAAI8C,KAAO,8BACX9C,IAAI+C,UAAY,aAEVC,SAAWT,KAAOC,OAAS,EAC3BS,SAAWR,IAAMC,QAAU,EAEjC1C,IAAIkD,SAASb,KAAMW,QAASC,SAC5BjD,IAAImD,cA/GJC,iBAAMpD,IAAK,CACXqD,KAAM,UACNxD,KAAM,CACFyD,SAAUhD,SAEdiD,QAAS,CACLC,QAAS,CACLC,MAAO,CACHlD,QAASA,QACT8B,KAAMtC,QACN+C,KAAM,CACFY,KAAM,GACNC,OAAQ,QAEZC,MAAO,OACPC,QAAS,CACLpB,IAAK,GACLC,OAAQ,IAEZoB,MAAO,UAEXC,OAAQ,CACJxD,SAAS,EACTyD,SAAU,SACVC,OAAQ,CACJC,eAAe,EACfC,WAAY,SACZN,QAAS,KAGjBO,QAAS,CACLC,gBAAiB,2BACjBC,WAAY,OACZC,UAAW,OACXC,YAAa,UACbC,YAAa,EACbC,eAAe,EACfC,UAAW,CACPlB,MAAO,SAASmB,gBACFA,QAAQ,GAAGC,IACZC,OAEbA,MAAO,SAASF,eACNG,EAAIH,QAAQC,UACX,CACF,SAAQhD,WAAWkD,EAAEC,KACrB,WAAUhD,KAAKiD,MAAiB,IAAXF,EAAEG,OAAe,KAAO,OAC7C,UAASH,EAAEI,QACX,QAAOJ,EAAEK,WAM9BC,OAAQ,CACJL,EAAG,CACCvB,MAAO,CACHlD,SAAS,EACT8B,KAAM,sBAEViD,IAAK,EACLC,MAAO,CACHC,SAAU,SAAS1D,cACRD,WAAWC,UAI9B2D,EAAG,CACChC,MAAO,CACHlD,SAAS,EACT8B,KAAM,gBAEViD,IAAK,EACLC,MAAO,CACHG,SAAU,OAK1BlC,QAAS,CAAChC"}
\ No newline at end of file
+{"version":3,"file":"scatter_chart.min.js","sources":["../src/scatter_chart.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * A module that creates a scatter chart to visualize student effort data using Chart.js.\n * The chart displays effort scores against time spent, with tooltips showing additional metrics.\n *\n * @module tiny_cursive/scatter_chart\n * @copyright 2025 Cursive Technology, Inc. \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Chart from 'core/chartjs';\nimport {get_strings as getStrings} from 'core/str';\nexport const init = async(data, apiKey, caption) => {\n\n const ctx = document.getElementById('effortScatterChart').getContext('2d');\n if (data) {\n data = JSON.parse(document.getElementById('scatter-chart-data').dataset.data);\n }\n\n let display = true;\n let isEmpty = \"\";\n var dataset = [];\n\n const [\n applyFilter,\n noSubmission,\n noPayload,\n freemium,\n time,\n words,\n effortratio,\n wpm,\n effortscore,\n timespent,\n ] = await getStrings([\n {key: 'apply_filter', component: 'tiny_cursive'},\n {key: 'no_submission', component: 'tiny_cursive'},\n {key: 'nopaylod', component: 'tiny_cursive'},\n {key: 'freemium', component: 'tiny_cursive'},\n {key: 'time', component: 'tiny_cursive'},\n {key: 'words', component: 'tiny_cursive'},\n {key: 'effort_ratio', component: 'tiny_cursive'},\n {key: 'wpm', component: 'tiny_cursive'},\n {key: 'effort_score', component: 'tiny_cursive'},\n {key: 'timespent', component: 'tiny_cursive'},\n ]);\n\n if (Array.isArray(data) && !data.state && apiKey) {\n dataset = data;\n isEmpty = data.some(ds =>\n Array.isArray(ds.data) &&\n ds.data.some(point =>\n point && typeof point === 'object' && Object.keys(point).length > 0\n )\n );\n }\n\n if (!apiKey || data.length === 0 || !isEmpty || data === false) {\n display = false;\n }\n\n const fallbackMessagePlugin = {\n id: 'fallbackMessagePlugin',\n afterDraw(chart) {\n // ⚠ Case 1: Freemium user\n if (!apiKey) {\n drawMessage('⚠ ' + freemium, chart);\n return;\n }\n // ⚠ Case 2: Apply filter (data is empty array)\n if (data.state == \"apply_filter\") {\n drawMessage('⚠ ' + applyFilter, chart);\n return;\n }\n if (data.state === \"no_submission\") {\n drawMessage('⚠ ' + noSubmission, chart);\n return;\n }\n // ⚠ Case 3: No payload data (all `data` arrays are empty or full of empty objects)\n if (!isEmpty && !data.state) {\n drawMessage('⚠ ' + noPayload, chart);\n }\n\n }\n };\n\n new Chart(ctx, {\n type: 'scatter',\n data: {\n datasets: dataset,\n },\n options: {\n plugins: {\n title: {\n display: display,\n text: caption,\n font: {\n size: 16,\n weight: 'bold',\n },\n color: '#333',\n padding: {\n top: 10,\n bottom: 20\n },\n align: 'center'\n },\n legend: {\n display: true,\n position: 'bottom',\n labels: {\n usePointStyle: true,\n pointStyle: 'circle',\n padding: 20\n }\n },\n tooltip: {\n backgroundColor: 'rgba(252, 252, 252, 0.8)',\n titleColor: '#000',\n bodyColor: '#000',\n borderColor: '#cccccc',\n borderWidth: 1,\n displayColors: false,\n callbacks: {\n title: function(context) {\n const d = context[0].raw;\n return d.label; // This appears as bold title.\n },\n label: function(context) {\n const d = context.raw;\n return [\n `${time}: ${formatTime(d.x)}`,\n `${effortratio}: ${Math.round(d.effort * 100 * 100) / 100}%`,\n `${words}: ${d.words}`,\n `${wpm}: ${d.wpm}`\n ];\n }\n }\n }\n },\n scales: {\n x: {\n title: {\n display: true,\n text: timespent\n },\n min: 0,\n ticks: {\n callback: function(value) {\n return formatTime(value);\n }\n }\n },\n y: {\n title: {\n display: true,\n text: effortscore\n },\n min: 0,\n ticks: {\n stepSize: 0.5\n }\n }\n }\n },\n plugins: [fallbackMessagePlugin]\n });\n\n /**\n * Formats a time value in seconds to a mm:ss string format\n * @param {number} value - The time value in seconds\n * @returns {string} The formatted time string in mm:ss format\n */\n function formatTime(value) {\n const minutes = Math.floor(value / 60);\n const seconds = value % 60;\n return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;\n }\n\n /**\n * Draws a message on the chart canvas\n * @param {string} text - The message to be displayed\n * @param {Chart} chart - The Chart.js chart object\n */\n function drawMessage(text, chart) {\n\n const {ctx, chartArea: {left, right, top, bottom}} = chart;\n ctx.save();\n ctx.textAlign = 'center';\n ctx.textBaseline = 'middle';\n ctx.font = 'bold 16px \"Segoe UI\", Arial';\n ctx.fillStyle = '#666';\n\n const centerX = (left + right) / 2;\n const centerY = (top + bottom) / 2;\n\n ctx.fillText(text, centerX, centerY);\n ctx.restore();\n }\n};"],"names":["async","data","apiKey","caption","ctx","document","getElementById","getContext","JSON","parse","dataset","display","isEmpty","applyFilter","noSubmission","noPayload","freemium","time","words","effortratio","wpm","effortscore","timespent","key","component","Array","isArray","state","some","ds","point","Object","keys","length","fallbackMessagePlugin","id","afterDraw","chart","drawMessage","formatTime","value","minutes","Math","floor","seconds","String","padStart","text","chartArea","left","right","top","bottom","save","textAlign","textBaseline","font","fillStyle","centerX","centerY","fillText","restore","Chart","type","datasets","options","plugins","title","size","weight","color","padding","align","legend","position","labels","usePointStyle","pointStyle","tooltip","backgroundColor","titleColor","bodyColor","borderColor","borderWidth","displayColors","callbacks","context","raw","label","d","x","round","effort","scales","min","ticks","callback","y","stepSize"],"mappings":";;;;;;;;0JA0BoBA,MAAMC,KAAMC,OAAQC,iBAE9BC,IAAMC,SAASC,eAAe,sBAAsBC,WAAW,MACjEN,OACAA,KAAOO,KAAKC,MAAMJ,SAASC,eAAe,sBAAsBI,QAAQT,WAGxEU,SAAU,EACVC,QAAU,OACVF,QAAU,SAGVG,YACAC,aACAC,UACAC,SACAC,KACAC,MACAC,YACAC,IACAC,YACAC,iBACM,oBAAW,CACjB,CAACC,IAAK,eAAgBC,UAAW,gBACjC,CAACD,IAAK,gBAAiBC,UAAW,gBAClC,CAACD,IAAK,WAAYC,UAAW,gBAC7B,CAACD,IAAK,WAAYC,UAAW,gBAC7B,CAACD,IAAK,OAAQC,UAAW,gBACzB,CAACD,IAAK,QAASC,UAAW,gBAC1B,CAACD,IAAK,eAAgBC,UAAW,gBACjC,CAACD,IAAK,MAAOC,UAAW,gBACxB,CAACD,IAAK,eAAgBC,UAAW,gBACjC,CAACD,IAAK,YAAaC,UAAW,kBAG9BC,MAAMC,QAAQzB,QAAUA,KAAK0B,OAASzB,SACtCQ,QAAUT,KACVW,QAAUX,KAAK2B,MAAKC,IAChBJ,MAAMC,QAAQG,GAAG5B,OACjB4B,GAAG5B,KAAK2B,MAAKE,OACTA,OAA0B,iBAAVA,OAAsBC,OAAOC,KAAKF,OAAOG,OAAS,OAKzE/B,QAA0B,IAAhBD,KAAKgC,QAAiBrB,UAAoB,IAATX,OAC5CU,SAAU,SAGRuB,sBAAwB,CAC1BC,GAAI,wBACJC,UAAUC,OAEDnC,OAKa,gBAAdD,KAAK0B,MAIU,kBAAf1B,KAAK0B,MAKJf,SAAYX,KAAK0B,OAClBW,YAAY,KAAOvB,UAAWsB,OAL9BC,YAAY,KAAOxB,aAAcuB,OAJjCC,YAAY,KAAOzB,YAAawB,OALhCC,YAAY,KAAOtB,SAAUqB,kBA2GhCE,WAAWC,aACVC,QAAUC,KAAKC,MAAMH,MAAQ,IAC7BI,QAAUJ,MAAQ,SAChB,GAAEK,OAAOJ,SAASK,SAAS,EAAG,QAAQD,OAAOD,SAASE,SAAS,EAAG,gBAQrER,YAAYS,KAAMV,aAEjBjC,IAACA,IAAK4C,WAAWC,KAACA,KAADC,MAAOA,MAAPC,IAAcA,IAAdC,OAAmBA,SAAWf,MACrDjC,IAAIiD,OACJjD,IAAIkD,UAAY,SAChBlD,IAAImD,aAAe,SACnBnD,IAAIoD,KAAO,8BACXpD,IAAIqD,UAAY,aAEVC,SAAWT,KAAOC,OAAS,EAC3BS,SAAWR,IAAMC,QAAU,EAEjChD,IAAIwD,SAASb,KAAMW,QAASC,SAC5BvD,IAAIyD,cA/GJC,iBAAM1D,IAAK,CACX2D,KAAM,UACN9D,KAAM,CACF+D,SAAUtD,SAEduD,QAAS,CACLC,QAAS,CACLC,MAAO,CACHxD,QAASA,QACToC,KAAM5C,QACNqD,KAAM,CACFY,KAAM,GACNC,OAAQ,QAEZC,MAAO,OACPC,QAAS,CACLpB,IAAK,GACLC,OAAQ,IAEZoB,MAAO,UAEXC,OAAQ,CACJ9D,SAAS,EACT+D,SAAU,SACVC,OAAQ,CACJC,eAAe,EACfC,WAAY,SACZN,QAAS,KAGjBO,QAAS,CACLC,gBAAiB,2BACjBC,WAAY,OACZC,UAAW,OACXC,YAAa,UACbC,YAAa,EACbC,eAAe,EACfC,UAAW,CACPlB,MAAO,SAASmB,gBACFA,QAAQ,GAAGC,IACZC,OAEbA,MAAO,SAASF,eACNG,EAAIH,QAAQC,UACX,CACF,GAAEtE,SAASsB,WAAWkD,EAAEC,KACxB,GAAEvE,gBAAgBuB,KAAKiD,MAAiB,IAAXF,EAAEG,OAAe,KAAO,OACrD,GAAE1E,UAAUuE,EAAEvE,QACd,GAAEE,QAAQqE,EAAErE,WAMjCyE,OAAQ,CACJH,EAAG,CACCvB,MAAO,CACHxD,SAAS,EACToC,KAAMzB,WAEVwE,IAAK,EACLC,MAAO,CACHC,SAAU,SAASxD,cACRD,WAAWC,UAI9ByD,EAAG,CACC9B,MAAO,CACHxD,SAAS,EACToC,KAAM1B,aAEVyE,IAAK,EACLC,MAAO,CACHG,SAAU,OAK1BhC,QAAS,CAAChC"}
\ No newline at end of file
diff --git a/amd/src/append_fourm_post.js b/amd/src/append_fourm_post.js
index 09ca2a82..36936fea 100644
--- a/amd/src/append_fourm_post.js
+++ b/amd/src/append_fourm_post.js
@@ -68,9 +68,27 @@ define(["jquery", "core/ajax", "core/str", "core/templates", "./replay", "./anal
var replyButton = $('a[data-region="post-action"][title="Reply"]');
if (replyButton.length > 0) {
replyButton.on('click', function(event) {
+ var isTeacher = $('#body').hasClass('teacher_admin');
+ if (isTeacher) {
+ return true;
+ }
event.preventDefault();
var url = $(this).attr('href');
- window.location.href = url;
+
+ var urlParts = url.split('#');
+ var baseUrl = urlParts[0];
+ var hash = urlParts.length > 1 ? '#' + urlParts[1] : '';
+
+ if (baseUrl.indexOf('setformat=') > -1) {
+ baseUrl = baseUrl.replace(/setformat=\d/, 'setformat=1');
+ } else if (baseUrl.indexOf('?') > -1) {
+ baseUrl += '&setformat=1';
+ } else {
+ baseUrl += '?setformat=1';
+ }
+ var finalUrl = baseUrl + hash;
+
+ window.location.href = finalUrl;
});
}
diff --git a/amd/src/append_submissions_table.js b/amd/src/append_submissions_table.js
index 08d00917..7cd36932 100644
--- a/amd/src/append_submissions_table.js
+++ b/amd/src/append_submissions_table.js
@@ -75,14 +75,14 @@ define([
appendTable: function(scoreSetting, hasApiKey) {
let subUrl = window.location.href;
let parm = new URL(subUrl);
- let hTr = $('thead').find('tr').get()[0];
+ let hTr = $('table#submissions thead').find('tr').get()[0];
Str.get_string('analytics', 'tiny_cursive')
.then(analyticString => {
$(hTr).find('th').eq(3).after('');
- $('tbody').find("tr").get().forEach(function(tr) {
+ $('table#submissions tbody').find("tr").get().forEach(function(tr) {
let tdUser = $(tr).find("td").get()[0];
let userid = $(tdUser).find("input[type='checkbox']")?.get()[0]?.value;
let cmid = parm.searchParams.get('id');
diff --git a/amd/src/scatter_chart.js b/amd/src/scatter_chart.js
index 47e933d0..60f40371 100644
--- a/amd/src/scatter_chart.js
+++ b/amd/src/scatter_chart.js
@@ -40,12 +40,23 @@ export const init = async(data, apiKey, caption) => {
noSubmission,
noPayload,
freemium,
+ time,
+ words,
+ effortratio,
+ wpm,
+ effortscore,
+ timespent,
] = await getStrings([
{key: 'apply_filter', component: 'tiny_cursive'},
{key: 'no_submission', component: 'tiny_cursive'},
{key: 'nopaylod', component: 'tiny_cursive'},
{key: 'freemium', component: 'tiny_cursive'},
- {key: 'chart_result', component: 'tiny_cursive'}
+ {key: 'time', component: 'tiny_cursive'},
+ {key: 'words', component: 'tiny_cursive'},
+ {key: 'effort_ratio', component: 'tiny_cursive'},
+ {key: 'wpm', component: 'tiny_cursive'},
+ {key: 'effort_score', component: 'tiny_cursive'},
+ {key: 'timespent', component: 'tiny_cursive'},
]);
if (Array.isArray(data) && !data.state && apiKey) {
@@ -132,10 +143,10 @@ export const init = async(data, apiKey, caption) => {
label: function(context) {
const d = context.raw;
return [
- `Time: ${formatTime(d.x)}`,
- `Effort: ${Math.round(d.effort * 100 * 100) / 100}%`,
- `Words: ${d.words}`,
- `WPM: ${d.wpm}`
+ `${time}: ${formatTime(d.x)}`,
+ `${effortratio}: ${Math.round(d.effort * 100 * 100) / 100}%`,
+ `${words}: ${d.words}`,
+ `${wpm}: ${d.wpm}`
];
}
}
@@ -145,7 +156,7 @@ export const init = async(data, apiKey, caption) => {
x: {
title: {
display: true,
- text: 'Time Spent (mm:ss)'
+ text: timespent
},
min: 0,
ticks: {
@@ -157,7 +168,7 @@ export const init = async(data, apiKey, caption) => {
y: {
title: {
display: true,
- text: 'Effort Score'
+ text: effortscore
},
min: 0,
ticks: {
diff --git a/classes/constants.php b/classes/constants.php
index c03270e6..073166fd 100644
--- a/classes/constants.php
+++ b/classes/constants.php
@@ -41,7 +41,7 @@ class constants {
* const array RUBRIC_AREA Mapping of module names to rubric areas
*/
public const RUBRIC_AREA = ['assign' => 'submissions', 'forum' => 'forum', 'quiz' => 'quiz', 'lesson' =>
- 'lesson'];
+ 'lesson'];
/**
* Array mapping page body IDs to their corresponding handler functions and module types.
diff --git a/classes/page/pdfexport.php b/classes/local/page/pdfexport.php
similarity index 99%
rename from classes/page/pdfexport.php
rename to classes/local/page/pdfexport.php
index 073ce316..dca72fc9 100644
--- a/classes/page/pdfexport.php
+++ b/classes/local/page/pdfexport.php
@@ -14,7 +14,7 @@
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see .
-namespace tiny_cursive\page;
+namespace tiny_cursive\local\page;
use context_course;
use context_module;
use html_writer;
diff --git a/classes/page/visualization.php b/classes/local/page/visualization.php
similarity index 99%
rename from classes/page/visualization.php
rename to classes/local/page/visualization.php
index 3aed20fd..0efb63a2 100644
--- a/classes/page/visualization.php
+++ b/classes/local/page/visualization.php
@@ -21,7 +21,7 @@
* @copyright 2025 Cursive Technology, Inc.
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-namespace tiny_cursive\page;
+namespace tiny_cursive\local\page;
use html_writer;
use moodle_url;
diff --git a/classes/privacy/provider.php b/classes/privacy/provider.php
index 46e2f63b..827974d7 100644
--- a/classes/privacy/provider.php
+++ b/classes/privacy/provider.php
@@ -66,7 +66,7 @@ public static function get_metadata(collection $collection): collection {
'timemodified' => 'privacy:metadata:database:tiny_cursive_comments:timemodified',
], 'privacy:metadata:database:tiny_cursive_comments');
- $collection->add_external_location_link('tiny_cursive_files', [
+ $collection->add_external_location_link('api.cursivetechnology.net', [
'userid' => 'privacy:metadata:database:tiny_cursive:userid',
'content' => 'privacy:metadata:database:tiny_cursive:content',
'original_content' => 'privacy:metadata:database:tiny_cursive:original_content',
diff --git a/db/install.php b/db/install.php
index 4036b797..973854f1 100644
--- a/db/install.php
+++ b/db/install.php
@@ -30,15 +30,15 @@
*/
function xmldb_tiny_cursive_install() {
- enable_webservice();
- enable_webservice_protocol();
+ tiny_cursive_enable_webservice();
+ tiny_cursive_enable_webservice_protocol();
}
/**
* Enable web services in Moodle
*
* @package tiny_cursive
*/
-function enable_webservice() {
+function tiny_cursive_enable_webservice() {
set_config('enablewebservices', 1);
}
@@ -47,6 +47,6 @@ function enable_webservice() {
*
* @package tiny_cursive
*/
-function enable_webservice_protocol() {
+function tiny_cursive_enable_webservice_protocol() {
set_config('webserviceprotocols', 'rest');
}
diff --git a/db/install.xml b/db/install.xml
index 8c520160..29e96c6f 100644
--- a/db/install.xml
+++ b/db/install.xml
@@ -20,6 +20,13 @@
+
+
+
+
+
+
+
@@ -36,6 +43,12 @@
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/db/upgrade.php b/db/upgrade.php
index dd3565b0..6ed4e920 100644
--- a/db/upgrade.php
+++ b/db/upgrade.php
@@ -224,5 +224,101 @@ function xmldb_tiny_cursive_upgrade($oldversion) {
upgrade_plugin_savepoint(true, 2024062004, 'tiny', 'cursive');
}
+ // Added Indexing into existing tables.
+ if ($oldversion < 2026013002) {
+ $table = new xmldb_table('tiny_cursive_files');
+
+ // Composite index for the most common query pattern (non-quiz modules).
+ $index = new xmldb_index(
+ 'idx_files_lookup',
+ XMLDB_INDEX_NOTUNIQUE,
+ ['cmid', 'modulename', 'resourceid', 'userid']
+ );
+ if (!$dbman->index_exists($table, $index)) {
+ $dbman->add_index($table, $index);
+ }
+
+ // Composite index for quiz queries that include questionid.
+ $index = new xmldb_index(
+ 'idx_files_quiz_lookup',
+ XMLDB_INDEX_NOTUNIQUE,
+ ['cmid', 'modulename', 'resourceid', 'userid', 'questionid']
+ );
+ if (!$dbman->index_exists($table, $index)) {
+ $dbman->add_index($table, $index);
+ }
+
+ // Supporting index for user-level queries.
+ $index = new xmldb_index('idx_files_userid', XMLDB_INDEX_NOTUNIQUE, ['userid']);
+ if (!$dbman->index_exists($table, $index)) {
+ $dbman->add_index($table, $index);
+ }
+
+ // Supporting index for course-level queries.
+ $index = new xmldb_index('idx_files_courseid', XMLDB_INDEX_NOTUNIQUE, ['courseid']);
+ if (!$dbman->index_exists($table, $index)) {
+ $dbman->add_index($table, $index);
+ }
+
+ // Index for upload status queries.
+ $index = new xmldb_index('idx_files_uploaded', XMLDB_INDEX_NOTUNIQUE, ['uploaded']);
+ if (!$dbman->index_exists($table, $index)) {
+ $dbman->add_index($table, $index);
+ }
+
+ $table = new xmldb_table('tiny_cursive_comments');
+
+ // Composite index for the most common query pattern (non-quiz modules).
+ $index = new xmldb_index(
+ 'idx_comments_lookup',
+ XMLDB_INDEX_NOTUNIQUE,
+ ['cmid', 'modulename', 'resourceid', 'userid']
+ );
+ if (!$dbman->index_exists($table, $index)) {
+ $dbman->add_index($table, $index);
+ }
+
+ // Composite index for quiz queries that include questionid.
+ $index = new xmldb_index(
+ 'idx_comments_quiz_lookup',
+ XMLDB_INDEX_NOTUNIQUE,
+ ['cmid', 'modulename', 'resourceid', 'userid', 'questionid']
+ );
+ if (!$dbman->index_exists($table, $index)) {
+ $dbman->add_index($table, $index);
+ }
+
+ // Supporting index for user-level queries.
+ $index = new xmldb_index('idx_comments_userid', XMLDB_INDEX_NOTUNIQUE, ['userid']);
+ if (!$dbman->index_exists($table, $index)) {
+ $dbman->add_index($table, $index);
+ }
+
+ // Supporting index for course-level queries.
+ $index = new xmldb_index('idx_comments_courseid', XMLDB_INDEX_NOTUNIQUE, ['courseid']);
+ if (!$dbman->index_exists($table, $index)) {
+ $dbman->add_index($table, $index);
+ }
+
+ $table = new xmldb_table('tiny_cursive_user_writing');
+
+ // Index for efficient joins with tiny_cursive_files.
+ $index = new xmldb_index('idx_user_writing_fileid', XMLDB_INDEX_NOTUNIQUE, ['file_id']);
+ if (!$dbman->index_exists($table, $index)) {
+ $dbman->add_index($table, $index);
+ }
+
+ $table = new xmldb_table('tiny_cursive_writing_diff');
+
+ // Index for efficient joins with tiny_cursive_files.
+ $index = new xmldb_index('idx_writing_diff_fileid', XMLDB_INDEX_NOTUNIQUE, ['file_id']);
+ if (!$dbman->index_exists($table, $index)) {
+ $dbman->add_index($table, $index);
+ }
+
+ // Savepoint reached.
+ upgrade_plugin_savepoint(true, 2026013002, 'tiny', 'cursive');
+ }
+
return true;
}
diff --git a/lang/en/tiny_cursive.php b/lang/en/tiny_cursive.php
index ac751902..3d37b540 100644
--- a/lang/en/tiny_cursive.php
+++ b/lang/en/tiny_cursive.php
@@ -98,6 +98,7 @@
$string['editspastesai'] = 'Edits/Pastes/AI';
$string['effort_ratio'] = 'Effort';
$string['effort_ratio_desc'] = 'Total characters from verified effort / total characters in submission';
+$string['effort_score'] = "Effort Score";
$string['email'] = 'Email';
$string['enable'] = 'Enable';
$string['enabled'] = 'Enabled';
@@ -237,10 +238,12 @@
$string['syncinterval_des'] = 'Specify how frequently (in seconds) the user\'s writing keystrokes should be synchronized with the server. A lower value provides more real-time tracking but may increase server load. Recommended range is 10-30 seconds.';
$string['test_token'] = 'Test token';
$string['thresold_description'] = 'Each site may set its threshold for providing the successful match “green check” to the TypeID column for student submissions. We recommend .65. However, there may be arguments for lower or higher thresholds depending on your experience or academic honesty policy.';
-$string['time_writing'] = 'Time writing ';
+$string['time'] = "Time";
+$string['time_writing'] = 'Time writing';
$string['time_writing_desc'] = 'Total duration less inactive periods';
$string['timeleft'] = 'Time Left';
$string['timesave_success'] = 'Time saved successfully';
+$string['timespent'] = "Time Spent (mm ss)";
$string['tiny_cursive'] = 'Authorship and Analytics';
$string['tiny_cursive_placeholder'] = 'Write your comment or paste your link here…';
$string['tiny_cursive_srcurl'] = 'Please provide a comment';
@@ -270,7 +273,9 @@
$string['word_count_des'] = 'How many words you typed is estimated based on your usage of the space bar.';
$string['word_len_mean'] = 'Average word length';
$string['word_len_mean_des'] = 'Average word length is calculated by dividing the estimated word count by the total number of characters. Word length varies based on your vocabulary, the audience that you\'re writing for, and the subject. Longer word lengths have an impact on readability and grade-level readability estimates.';
+$string['words'] = "Words";
$string['words_per_minute'] = 'Writing speed';
$string['words_per_minute_desc'] = 'Words per minute';
+$string['wpm'] = "WPM";
$string['wractivityreport'] = 'Writing activity report';
$string['writing_analytics'] = "Writing Analytics";
diff --git a/lib.php b/lib.php
index e96bba8b..a85686fc 100644
--- a/lib.php
+++ b/lib.php
@@ -302,7 +302,7 @@ function tiny_cursive_upload_multipart_record($filerecord, $filenamewithfullpath
$remoteurl = get_config('tiny_cursive', 'python_server') . "/upload_file";
$filetosend = '';
- $tempfilepath = tempnam(sys_get_temp_dir(), 'upload');
+ $tempfilepath = make_temp_directory('tiny_cursive') . '/' . uniqid('upload_', true);
$jsoncontent = json_decode($filerecord->content, true);
diff --git a/my_writing_report.php b/my_writing_report.php
index c70bba3c..5526d31c 100644
--- a/my_writing_report.php
+++ b/my_writing_report.php
@@ -89,7 +89,7 @@
$PAGE->requires->js_call_amd('tiny_cursive/key_logger', 'init', [1]);
$PAGE->requires->js_call_amd('tiny_cursive/cursive_writing_reports', 'init', ["", constants::has_api_key(),
- get_config('tiny_cursive', 'json_download')]);
+ get_config('tiny_cursive', 'json_download')]);
$PAGE->set_context(context_system::instance());
$PAGE->set_url($url);
diff --git a/pdfexport.php b/pdfexport.php
index 55bfe13d..6c6780a4 100644
--- a/pdfexport.php
+++ b/pdfexport.php
@@ -32,5 +32,5 @@
$cmid = required_param('cmid', PARAM_INT);
$course = required_param('course', PARAM_INT);
$qid = optional_param('qid', 0, PARAM_INT);
-$page = new \tiny_cursive\page\pdfexport($course, $cmid, $id, $qid, $file);
+$page = new \tiny_cursive\local\page\pdfexport($course, $cmid, $id, $qid, $file);
$page->download();
diff --git a/thirdpartylibs.xml b/thirdpartylibs.xml
new file mode 100644
index 00000000..3cc20824
--- /dev/null
+++ b/thirdpartylibs.xml
@@ -0,0 +1,16 @@
+
+
+
+ amd/js/html2pdf.js
+ html2pdf
+ 0.10.1
+ MIT
+
+
+ Copyright (c) 2019–present eKoopmans
+
+
+ https://github.com/eKoopmans/html2pdf.js
+
+
+
diff --git a/tiny_cursive_report.php b/tiny_cursive_report.php
index 6df7a32d..b14f40b8 100644
--- a/tiny_cursive_report.php
+++ b/tiny_cursive_report.php
@@ -125,7 +125,7 @@
$moduleid,
$userid
);
- $chart = new \tiny_cursive\page\visualization($courseid, "", $moduleid, $formdata->userid);
+ $chart = new \tiny_cursive\local\page\visualization($courseid, "", $moduleid, $formdata->userid);
$chart->render();
} else {
$users = tiny_cursive_get_user_attempts_data(
@@ -146,7 +146,7 @@
$moduleid,
$userid
);
- $chart = new \tiny_cursive\page\visualization($courseid, "", $moduleid, $userid);
+ $chart = new \tiny_cursive\local\page\visualization($courseid, "", $moduleid, $userid);
$chart->render();
}
diff --git a/version.php b/version.php
index d8027c3e..ace6bacb 100644
--- a/version.php
+++ b/version.php
@@ -29,6 +29,6 @@
$plugin->component = 'tiny_cursive';
$plugin->release = '2.1.3';
-$plugin->version = 2026013000;
+$plugin->version = 2026013100;
$plugin->requires = 2022041912;
$plugin->maturity = MATURITY_STABLE;