diff --git a/amd/build/analytic_button.min.js.map b/amd/build/analytic_button.min.js.map
index 36c975cc..105c1ce0 100644
--- a/amd/build/analytic_button.min.js.map
+++ b/amd/build/analytic_button.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"analytic_button.min.js","sources":["../src/analytic_button.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 that creates an analytics button with an icon and text.\n * The button displays analytics information for a specific user and question.\n *\n * @module tiny_cursive/analytic_button\n * @copyright 2024 CTI \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine([\"core/str\"], function(Str) {\n const analyticButton = (effort, userid, questionid = \"\") => {\n const anchor = document.createElement(\"a\");\n anchor.href = \"#\";\n anchor.id = \"analytics\" + userid + questionid;\n anchor.classList.add(\n \"d-inline-flex\",\n \"justify-content-center\",\n 'text-decoration-none'\n );\n\n const button = document.createElement('div');\n button.className = 'tiny_cursive-analytics-button';\n\n // Left side (icon + label)\n const left = document.createElement('div');\n left.className = 'tiny_cursive-analytics-left';\n\n const icon = document.createElement('img');\n icon.src = M.util.image_url('chart-column', 'tiny_cursive');\n icon.alt = 'Analytics Icon';\n icon.className = 'tiny_cursive-analytics-bar-icon';\n\n const label = document.createElement('span');\n label.className = 'tiny_cursive-analytics-label';\n label.textContent = 'Analytics';\n Str.get_string(\"analytics\", \"tiny_cursive\")\n .then((analyticsString) => {\n label.textContent = analyticsString;\n return analyticsString;\n })\n .catch((error) => {\n window.console.error(\"Error fetching string:\", error);\n });\n\n left.appendChild(icon);\n left.appendChild(label);\n button.appendChild(left);\n\n if (effort) {\n const right = document.createElement('div');\n right.className = 'tiny_cursive-analytics-right';\n right.textContent = effort + \"%\";\n right.title = \"Effort\";\n\n if (effort < 90) {\n right.style.backgroundColor = '#EAB308';\n }\n\n button.appendChild(right);\n }\n // Compose full button\n anchor.appendChild(button);\n\n return anchor;\n };\n\n return analyticButton;\n});\n"],"names":["define","Str","effort","userid","questionid","anchor","document","createElement","href","id","classList","add","button","className","left","icon","src","M","util","image_url","alt","label","textContent","get_string","then","analyticsString","catch","error","window","console","appendChild","right","title","style","backgroundColor"],"mappings":";;;;;;;;AAwBAA,sCAAO,CAAC,aAAa,SAASC,YACL,SAACC,OAAQC,YAAQC,kEAAa,SAC7CC,OAASC,SAASC,cAAc,KACtCF,OAAOG,KAAO,IACdH,OAAOI,GAAK,YAAcN,OAASC,WACnCC,OAAOK,UAAUC,IACf,gBACA,yBACA,8BAGIC,OAASN,SAASC,cAAc,OACtCK,OAAOC,UAAY,sCAGbC,KAAOR,SAASC,cAAc,OACpCO,KAAKD,UAAY,oCAEXE,KAAOT,SAASC,cAAc,OACpCQ,KAAKC,IAAMC,EAAEC,KAAKC,UAAU,eAAgB,gBAC5CJ,KAAKK,IAAM,iBACXL,KAAKF,UAAY,wCAEXQ,MAAQf,SAASC,cAAc,WACrCc,MAAMR,UAAY,+BAClBQ,MAAMC,YAAc,YACpBrB,IAAIsB,WAAW,YAAa,gBACzBC,MAAMC,kBACLJ,MAAMC,YAAcG,gBACbA,mBAERC,OAAOC,QACNC,OAAOC,QAAQF,MAAM,yBAA0BA,UAGnDb,KAAKgB,YAAYf,MACjBD,KAAKgB,YAAYT,OACjBT,OAAOkB,YAAYhB,MAEfZ,OAAQ,OACJ6B,MAAQzB,SAASC,cAAc,OACrCwB,MAAMlB,UAAY,+BAClBkB,MAAMT,YAAcpB,OAAS,IAC7B6B,MAAMC,MAAQ,SAEV9B,OAAS,KACX6B,MAAME,MAAMC,gBAAkB,WAGhCtB,OAAOkB,YAAYC,cAGrB1B,OAAOyB,YAAYlB,QAEZP"}
\ No newline at end of file
+{"version":3,"file":"analytic_button.min.js","sources":["../src/analytic_button.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\r\n//\r\n// Moodle is free software: you can redistribute it and/or modify\r\n// it under the terms of the GNU General Public License as published by\r\n// the Free Software Foundation, either version 3 of the License, or\r\n// (at your option) any later version.\r\n//\r\n// Moodle is distributed in the hope that it will be useful,\r\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\r\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\r\n// GNU General Public License for more details.\r\n//\r\n// You should have received a copy of the GNU General Public License\r\n// along with Moodle. If not, see .\r\n\r\n/**\r\n * Module that creates an analytics button with an icon and text.\r\n * The button displays analytics information for a specific user and question.\r\n *\r\n * @module tiny_cursive/analytic_button\r\n * @copyright 2024 CTI \r\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\r\n */\r\n\r\ndefine([\"core/str\"], function(Str) {\r\n const analyticButton = (effort, userid, questionid = \"\") => {\r\n const anchor = document.createElement(\"a\");\r\n anchor.href = \"#\";\r\n anchor.id = \"analytics\" + userid + questionid;\r\n anchor.classList.add(\r\n \"d-inline-flex\",\r\n \"justify-content-center\",\r\n 'text-decoration-none'\r\n );\r\n\r\n const button = document.createElement('div');\r\n button.className = 'tiny_cursive-analytics-button';\r\n\r\n // Left side (icon + label)\r\n const left = document.createElement('div');\r\n left.className = 'tiny_cursive-analytics-left';\r\n\r\n const icon = document.createElement('img');\r\n icon.src = M.util.image_url('chart-column', 'tiny_cursive');\r\n icon.alt = 'Analytics Icon';\r\n icon.className = 'tiny_cursive-analytics-bar-icon';\r\n\r\n const label = document.createElement('span');\r\n label.className = 'tiny_cursive-analytics-label';\r\n label.textContent = 'Analytics';\r\n Str.get_string(\"analytics\", \"tiny_cursive\")\r\n .then((analyticsString) => {\r\n label.textContent = analyticsString;\r\n return analyticsString;\r\n })\r\n .catch((error) => {\r\n window.console.error(\"Error fetching string:\", error);\r\n });\r\n\r\n left.appendChild(icon);\r\n left.appendChild(label);\r\n button.appendChild(left);\r\n\r\n if (effort) {\r\n const right = document.createElement('div');\r\n right.className = 'tiny_cursive-analytics-right';\r\n right.textContent = effort + \"%\";\r\n right.title = \"Effort\";\r\n\r\n if (effort < 90) {\r\n right.style.backgroundColor = '#EAB308';\r\n }\r\n\r\n button.appendChild(right);\r\n }\r\n // Compose full button\r\n anchor.appendChild(button);\r\n\r\n return anchor;\r\n };\r\n\r\n return analyticButton;\r\n});\r\n"],"names":["define","Str","effort","userid","questionid","anchor","document","createElement","href","id","classList","add","button","className","left","icon","src","M","util","image_url","alt","label","textContent","get_string","then","analyticsString","catch","error","window","console","appendChild","right","title","style","backgroundColor"],"mappings":";;;;;;;;AAwBAA,sCAAO,CAAC,aAAa,SAASC,YACL,SAACC,OAAQC,YAAQC,kEAAa,SAC7CC,OAASC,SAASC,cAAc,KACtCF,OAAOG,KAAO,IACdH,OAAOI,GAAK,YAAcN,OAASC,WACnCC,OAAOK,UAAUC,IACf,gBACA,yBACA,8BAGIC,OAASN,SAASC,cAAc,OACtCK,OAAOC,UAAY,sCAGbC,KAAOR,SAASC,cAAc,OACpCO,KAAKD,UAAY,oCAEXE,KAAOT,SAASC,cAAc,OACpCQ,KAAKC,IAAMC,EAAEC,KAAKC,UAAU,eAAgB,gBAC5CJ,KAAKK,IAAM,iBACXL,KAAKF,UAAY,wCAEXQ,MAAQf,SAASC,cAAc,WACrCc,MAAMR,UAAY,+BAClBQ,MAAMC,YAAc,YACpBrB,IAAIsB,WAAW,YAAa,gBACzBC,MAAMC,kBACLJ,MAAMC,YAAcG,gBACbA,mBAERC,OAAOC,QACNC,OAAOC,QAAQF,MAAM,yBAA0BA,UAGnDb,KAAKgB,YAAYf,MACjBD,KAAKgB,YAAYT,OACjBT,OAAOkB,YAAYhB,MAEfZ,OAAQ,OACJ6B,MAAQzB,SAASC,cAAc,OACrCwB,MAAMlB,UAAY,+BAClBkB,MAAMT,YAAcpB,OAAS,IAC7B6B,MAAMC,MAAQ,SAEV9B,OAAS,KACX6B,MAAME,MAAMC,gBAAkB,WAGhCtB,OAAOkB,YAAYC,cAGrB1B,OAAOyB,YAAYlB,QAEZP"}
\ No newline at end of file
diff --git a/amd/build/analytic_events.min.js.map b/amd/build/analytic_events.min.js.map
index 321173da..cd731fab 100644
--- a/amd/build/analytic_events.min.js.map
+++ b/amd/build/analytic_events.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"analytic_events.min.js","sources":["../src/analytic_events.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 for handling analytics events in the Tiny Cursive plugin.\n * Provides functionality for displaying analytics data, replaying writing,\n * checking differences and showing quality metrics.\n *\n * @module tiny_cursive/analytic_events\n * @copyright 2024 CTI \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport myModal from \"./analytic_modal\";\nimport {call as getContent} from \"core/ajax\";\nimport $ from 'jquery';\nimport {get_string as getString} from 'core/str';\nimport {get_strings as getStrings} from 'core/str';\nimport template from 'core/templates';\n\nexport default class AnalyticEvents {\n\n constructor() {\n getString('notenoughtinfo', 'tiny_cursive').then(str => {\n localStorage.setItem('notenoughtinfo', str);\n return str;\n }).catch(error => window.console.log(error));\n }\n\n createModal(userid, context, questionid = '', replayInstances = null, authIcon) {\n const self = this;\n $('#analytics' + userid + questionid).on('click', function(e) {\n e.preventDefault();\n\n const isReplayButton = $(this).find('.tiny_cursive-replay-button').length > 0;\n // Create Moodle modal\n myModal.create({templateContext: context}).then(modal => {\n $('#content' + userid + ' .tiny_cursive_table tbody tr:first-child td:nth-child(2)').html(authIcon);\n modal.show();\n\n if (isReplayButton) {\n setTimeout(() => {\n $('.tiny_cursive-nav-tab').find('.active').removeClass('active');\n\n const replayTab = $('#rep' + userid + questionid);\n if (replayTab.length) {\n replayTab.trigger('click');\n replayTab.addClass('active');\n }\n }, 50);\n }\n\n let moreBtn = $('body #more' + userid + questionid);\n if (moreBtn.length > 0) {\n $('.tiny_cursive-nav-tab').find('.active').removeClass('active');\n $('#analytic' + userid + questionid).prop('disabled', true);\n $('#diff' + userid + questionid).prop('disabled', true);\n $('#analytic' + userid + questionid).css({\n 'background-color': 'rgba(168, 168, 168, 0.133)',\n 'cursor': 'not-allowed'\n });\n $('#diff' + userid + questionid).css({\n 'background-color': 'rgba(168, 168, 168, 0.133)',\n 'cursor': 'not-allowed'\n });\n moreBtn.on('click', function(e) {\n e.preventDefault();\n self.learnMore($(this), context, userid, questionid, replayInstances);\n });\n }\n\n return true;\n }).catch(error => {\n window.console.error(\"Failed to create modal:\", error);\n });\n\n });\n }\n\n analytics(userid, templates, context, questionid = '', replayInstances = null, authIcon) {\n\n $('body').on('click', '#analytic' + userid + questionid, function(e) {\n $('#rep' + userid + questionid).prop('disabled', false);\n $('#quality' + userid + questionid).prop('disabled', false);\n $('#content' + userid).attr('data-label', 'analytics');\n $('#player_' + userid + questionid).css({'display': 'none'});\n $('#content' + userid).removeClass('tiny_cursive_outputElement')\n .addClass('tiny_cursive').attr('data-label', 'analytics');\n e.preventDefault();\n $('#content' + userid).html($('
').addClass('tiny_cursive-loader')));\r\n $('.tiny_cursive-nav-tab').find('.active').removeClass('active');\r\n $(this).addClass('active'); // Add 'active' class to the clicked element\r\n if (replayInstances && replayInstances[userid]) {\r\n replayInstances[userid].stopReplay();\r\n }\r\n if (questionid) {\r\n // eslint-disable-next-line\r\n video_playback(userid, filepath, questionid);\r\n } else {\r\n // eslint-disable-next-line\r\n video_playback(userid, filepath);\r\n }\r\n });\r\n }\r\n\r\n learnMore(moreBtn, context, userid, questionid, replayInstances) {\r\n $('.tiny_cursive-nav-tab').find('.active').removeClass('active');\r\n moreBtn.addClass('active');\r\n $('#rep' + userid + questionid).prop('disabled', false);\r\n if (replayInstances && replayInstances[userid]) {\r\n replayInstances[userid].stopReplay();\r\n }\r\n $('#content' + userid + questionid).removeClass('tiny_cursive_outputElement');\r\n $('#replayControls_' + userid + questionid).addClass('d-none');\r\n template.render('tiny_cursive/learn_more', context).then(function(html) {\r\n $('#content' + userid + questionid).html(html);\r\n return true;\r\n }).fail(function(error) {\r\n window.console.error(\"Failed to render template:\", error);\r\n });\r\n }\r\n\r\n formatedTime(data) {\r\n if (data.total_time_seconds) {\r\n let totalTimeSeconds = data.total_time_seconds;\r\n let hours = Math.floor(totalTimeSeconds / 3600).toString().padStart(2, 0);\r\n let minutes = Math.floor((totalTimeSeconds % 3600) / 60).toString().padStart(2, 0);\r\n let seconds = (totalTimeSeconds % 60).toString().padStart(2, 0);\r\n return `${hours}h ${minutes}m ${seconds}s`;\r\n } else {\r\n return \"0h 0m 0s\";\r\n }\r\n }\r\n\r\n authorshipStatus(firstFile, score, scoreSetting) {\r\n var icon = 'fa fa-circle-o';\r\n var color = 'font-size:32px;color:black';\r\n score = parseFloat(score);\r\n\r\n if (firstFile) {\r\n icon = 'fa fa-solid fa-info-circle';\r\n color = 'font-size:32px;color:#000000';\r\n } else if (score >= scoreSetting) {\r\n icon = 'fa fa-check-circle';\r\n color = 'font-size:32px;color:green';\r\n }\r\n if (score < scoreSetting) {\r\n icon = 'fa fa-question-circle';\r\n color = 'font-size:32px;color:#A9A9A9';\r\n return $('').addClass(icon).attr('style', color).attr('title', localStorage.getItem('notenoughtinfo'));\r\n } else {\r\n return $('').addClass(icon).attr('style', color);\r\n }\r\n\r\n }\r\n}\r\n"],"names":["constructor","then","str","localStorage","setItem","catch","error","window","console","log","createModal","userid","context","questionid","replayInstances","authIcon","self","this","on","e","preventDefault","isReplayButton","find","length","create","templateContext","modal","html","show","setTimeout","removeClass","replayTab","trigger","addClass","moreBtn","prop","css","learnMore","analytics","templates","attr","append","stopReplay","render","fail","checkDiff","fileid","nodata","document","createElement","classList","add","textContent","Error","methodname","args","done","response","responsedata","JSON","parse","data","submittedText","atob","submitted_text","key","component","strings","originalTextString","editsPastesAIString","commentBox","pasteCountDiv","commentscount","commentsDiv","commentsList","comments","index","commentDiv","text","usercomment","$legend","$attributedItem","$attributedBox","$attributedText","$unattributedItem","$unattributedBox","$unattributedText","contents","textBlock2","message","replyWriting","filepath","video_playback","formatedTime","total_time_seconds","totalTimeSeconds","Math","floor","toString","padStart","authorshipStatus","firstFile","score","scoreSetting","icon","color","parseFloat","getItem"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/build/analytic_modal.min.js.map b/amd/build/analytic_modal.min.js.map
index 068d5e6b..39960f9e 100644
--- a/amd/build/analytic_modal.min.js.map
+++ b/amd/build/analytic_modal.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"analytic_modal.min.js","sources":["../src/analytic_modal.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 * This module defines a custom modal for analytics.\n *\n * @module tiny_cursive/analytic_modal\n * @copyright 2024 CTI \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Modal from 'core/modal';\nimport $ from 'jquery';\nexport default class MyModal extends Modal {\n static TYPE = \"tiny_cursive/analytics_modal\";\n static TEMPLATE = \"tiny_cursive/analytics_modal\";\n\n configure(modalConfig) {\n // Show this modal on instantiation.\n modalConfig.show = true;\n\n // Remove from the DOM on close.\n modalConfig.removeOnClose = true;\n modalConfig.backdrop = true;\n\n // Call the parent configure method.\n super.configure(modalConfig);\n }\n\n // Override the parent show method to add custom behavior.\n show() {\n super.show();\n\n const root = this.getRoot();\n\n\n // Hide the default modal header.\n root.find('.modal-header').remove();\n\n root.find('.modal-content').css({\n 'border-radius': '30px'\n }).addClass('shadow-none border-none');\n // Remove padding from the modal content.\n root.find('.modal-body').css({\n 'padding': '0',\n 'border-radius': '30px',\n 'overflow': 'auto',\n 'scrollbar-width': 'none'\n });\n root.find('.modal-dialog').css({\n 'max-width': '800px',\n 'background-color': 'transparent'\n });\n\n // Ensure modal closes on 'analytic-close' button click.\n root.find('#analytic-close').on('click', () => {\n this.destroy();\n });\n\n // Ensure modal closes on backdrop click.\n root.on('click', (e) => {\n if ($(e.target).hasClass('modal')) {\n this.destroy();\n }\n });\n }\n}\n"],"names":["MyModal","Modal","configure","modalConfig","show","removeOnClose","backdrop","root","this","getRoot","find","remove","css","addClass","on","destroy","e","target","hasClass"],"mappings":";;;;;;;yKAyBqBA,gBAAgBC,2BACnB,+CACI,+BAElBC,UAAUC,aAENA,YAAYC,MAAO,EAGnBD,YAAYE,eAAgB,EAC5BF,YAAYG,UAAW,QAGjBJ,UAAUC,aAIpBC,aACUA,aAEAG,KAAOC,KAAKC,UAIlBF,KAAKG,KAAK,iBAAiBC,SAE3BJ,KAAKG,KAAK,kBAAkBE,IAAI,iBACX,SAClBC,SAAS,2BAEZN,KAAKG,KAAK,eAAeE,IAAI,SACd,oBACM,gBACL,yBACO,SAEvBL,KAAKG,KAAK,iBAAiBE,IAAI,aACd,2BACO,gBAIxBL,KAAKG,KAAK,mBAAmBI,GAAG,SAAS,UAChCC,aAITR,KAAKO,GAAG,SAAUE,KACV,mBAAEA,EAAEC,QAAQC,SAAS,eAChBH"}
\ No newline at end of file
+{"version":3,"file":"analytic_modal.min.js","sources":["../src/analytic_modal.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\r\n//\r\n// Moodle is free software: you can redistribute it and/or modify\r\n// it under the terms of the GNU General Public License as published by\r\n// the Free Software Foundation, either version 3 of the License, or\r\n// (at your option) any later version.\r\n//\r\n// Moodle is distributed in the hope that it will be useful,\r\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\r\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\r\n// GNU General Public License for more details.\r\n//\r\n// You should have received a copy of the GNU General Public License\r\n// along with Moodle. If not, see .\r\n\r\n/**\r\n * This module defines a custom modal for analytics.\r\n *\r\n * @module tiny_cursive/analytic_modal\r\n * @copyright 2024 CTI \r\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\r\n */\r\n\r\nimport Modal from 'core/modal';\r\nimport $ from 'jquery';\r\nexport default class MyModal extends Modal {\r\n static TYPE = \"tiny_cursive/analytics_modal\";\r\n static TEMPLATE = \"tiny_cursive/analytics_modal\";\r\n\r\n configure(modalConfig) {\r\n // Show this modal on instantiation.\r\n modalConfig.show = true;\r\n\r\n // Remove from the DOM on close.\r\n modalConfig.removeOnClose = true;\r\n modalConfig.backdrop = true;\r\n\r\n // Call the parent configure method.\r\n super.configure(modalConfig);\r\n }\r\n\r\n // Override the parent show method to add custom behavior.\r\n show() {\r\n super.show();\r\n\r\n const root = this.getRoot();\r\n\r\n\r\n // Hide the default modal header.\r\n root.find('.modal-header').remove();\r\n\r\n root.find('.modal-content').css({\r\n 'border-radius': '30px'\r\n }).addClass('shadow-none border-none');\r\n // Remove padding from the modal content.\r\n root.find('.modal-body').css({\r\n 'padding': '0',\r\n 'border-radius': '30px',\r\n 'overflow': 'auto',\r\n 'scrollbar-width': 'none'\r\n });\r\n root.find('.modal-dialog').css({\r\n 'max-width': '800px',\r\n 'background-color': 'transparent'\r\n });\r\n\r\n // Ensure modal closes on 'analytic-close' button click.\r\n root.find('#analytic-close').on('click', () => {\r\n this.destroy();\r\n });\r\n\r\n // Ensure modal closes on backdrop click.\r\n root.on('click', (e) => {\r\n if ($(e.target).hasClass('modal')) {\r\n this.destroy();\r\n }\r\n });\r\n }\r\n}\r\n"],"names":["MyModal","Modal","configure","modalConfig","show","removeOnClose","backdrop","root","this","getRoot","find","remove","css","addClass","on","destroy","e","target","hasClass"],"mappings":";;;;;;;yKAyBqBA,gBAAgBC,2BACnB,+CACI,+BAElBC,UAAUC,aAENA,YAAYC,MAAO,EAGnBD,YAAYE,eAAgB,EAC5BF,YAAYG,UAAW,QAGjBJ,UAAUC,aAIpBC,aACUA,aAEAG,KAAOC,KAAKC,UAIlBF,KAAKG,KAAK,iBAAiBC,SAE3BJ,KAAKG,KAAK,kBAAkBE,IAAI,iBACX,SAClBC,SAAS,2BAEZN,KAAKG,KAAK,eAAeE,IAAI,SACd,oBACM,gBACL,yBACO,SAEvBL,KAAKG,KAAK,iBAAiBE,IAAI,aACd,2BACO,gBAIxBL,KAAKG,KAAK,mBAAmBI,GAAG,SAAS,UAChCC,aAITR,KAAKO,GAAG,SAAUE,KACV,mBAAEA,EAAEC,QAAQC,SAAS,eAChBH"}
\ 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..edfce93c 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/\r\n//\r\n// Moodle is free software: you can redistribute it and/or modify\r\n// it under the terms of the GNU General Public License as published by\r\n// the Free Software Foundation, either version 3 of the License, or\r\n// (at your option) any later version.\r\n//\r\n// Moodle is distributed in the hope that it will be useful,\r\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\r\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\r\n// GNU General Public License for more details.\r\n//\r\n// You should have received a copy of the GNU General Public License\r\n// along with Moodle. If not, see .\r\n\r\n/**\r\n * @module tiny_cursive/append_fourm_post\r\n * @category TinyMCE Editor\r\n * @copyright CTI \r\n * @author kuldeep singh \r\n */\r\n\r\ndefine([\"jquery\", \"core/ajax\", \"core/str\", \"core/templates\", \"./replay\", \"./analytic_button\",\r\n \"./replay_button\", \"./analytic_events\"], function(\r\n $,\r\n AJAX,\r\n str,\r\n templates,\r\n Replay,\r\n analyticButton,\r\n replayButton,\r\n AnalyticEvents\r\n) {\r\n const replayInstances = {};\r\n // eslint-disable-next-line camelcase\r\n window.video_playback = function(mid, filepath) {\r\n if (filepath !== '') {\r\n const replay = new Replay(\r\n 'content' + mid,\r\n filepath,\r\n 10,\r\n false,\r\n 'player_' + mid\r\n );\r\n replayInstances[mid] = replay;\r\n } else {\r\n templates.render('tiny_cursive/no_submission').then(html => {\r\n $('#content' + mid).html(html);\r\n return true;\r\n }).catch(e => window.console.error(e));\r\n }\r\n return false;\r\n\r\n };\r\n\r\n var usersTable = {\r\n init: function(scoreSetting, showcomment, hasApiKey) {\r\n str\r\n .get_strings([\r\n {key: \"field_require\", component: \"tiny_cursive\"},\r\n ])\r\n .done(function() {\r\n usersTable.getToken(scoreSetting, showcomment, hasApiKey);\r\n });\r\n },\r\n getToken: function(scoreSetting, showcomment, hasApiKey) {\r\n $('#page-mod-forum-discuss').find(\"article\").get().forEach(function(entry) {\r\n var replyButton = $('a[data-region=\"post-action\"][title=\"Reply\"]');\r\n if (replyButton.length > 0) {\r\n replyButton.on('click', function(event) {\r\n event.preventDefault();\r\n var url = $(this).attr('href');\r\n window.location.href = url;\r\n });\r\n }\r\n\r\n var ids = $(\"#\" + entry.id).data(\"post-id\");\r\n var cmid = M.cfg.contextInstanceId;\r\n\r\n let args = {id: ids, modulename: \"forum\", cmid: cmid};\r\n let methodname = 'cursive_get_forum_comment_link';\r\n let com = AJAX.call([{methodname, args}]);\r\n com[0].done(function(json) {\r\n var data = JSON.parse(json);\r\n\r\n var filepath = '';\r\n if (data.data.filename) {\r\n filepath = data.data.filename;\r\n }\r\n if (filepath) {\r\n\r\n let analyticButtonDiv = document.createElement('div');\r\n\r\n if (!hasApiKey) {\r\n $(analyticButtonDiv).html(replayButton(ids));\r\n } else {\r\n analyticButtonDiv.append(analyticButton(data.data.effort_ratio, ids));\r\n }\r\n\r\n analyticButtonDiv.classList.add('text-center', 'my-2');\r\n analyticButtonDiv.dataset.region = \"analytic-div\" + ids;\r\n\r\n $(\"#\" + entry.id).find('#post-content-' + ids).prepend(analyticButtonDiv);\r\n\r\n let myEvents = new AnalyticEvents();\r\n var context = {\r\n tabledata: data.data,\r\n formattime: myEvents.formatedTime(data.data),\r\n page: scoreSetting,\r\n userid: ids,\r\n apikey: hasApiKey\r\n };\r\n\r\n let authIcon = myEvents.authorshipStatus(data.data.first_file, data.data.score, scoreSetting);\r\n myEvents.createModal(ids, context, '', replayInstances, authIcon);\r\n myEvents.analytics(ids, templates, context, '', replayInstances, authIcon);\r\n myEvents.checkDiff(ids, data.data.file_id, '', replayInstances);\r\n myEvents.replyWriting(ids, filepath, '', replayInstances);\r\n }\r\n\r\n });\r\n return com.usercomment;\r\n });\r\n },\r\n };\r\n return usersTable;\r\n\r\n\r\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
diff --git a/amd/build/append_lesson_grade_table.min.js.map b/amd/build/append_lesson_grade_table.min.js.map
index ac5fede6..14e460ed 100644
--- a/amd/build/append_lesson_grade_table.min.js.map
+++ b/amd/build/append_lesson_grade_table.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"append_lesson_grade_table.min.js","sources":["../src/append_lesson_grade_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 to append analytics and grade table data for lesson submissions\n * Handles the display of analytics, replay functionality and grade information\n * for lesson submissions in the Moodle gradebook interface\n *\n * @module tiny_cursive/append_lesson_grade_table\n * @copyright 2025 CTI \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Replay from './replay';\nimport $ from 'jquery';\nimport {call as getData} from 'core/ajax';\nimport templates from 'core/templates';\nimport AnalyticEvents from './analytic_events';\nimport analyticButton from './analytic_button';\nimport replayButton from './replay_button';\nimport * as Str from 'core/str';\n\nexport const init = (scoreSetting, showcomment, hasApiKey) => {\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\n var cmid = M.cfg.contextInstanceId;\n var emailLink = $('#page-content div[role=\"main\"] td.lastcol a');\n var headcolumn = $('#region-main div[role=\"main\"] table thead tr');\n let url = new URL(window.location.href);\n let mode = url.searchParams.get('mode');\n let user = url.searchParams.get('user');\n\n Str.get_string('analytics', 'tiny_cursive').then((strs) => {\n headcolumn.each(function() {\n $(this).find('th:eq(1)').after(`
${strs}
`);\n });\n return true;\n }).catch(e => window.console.error(e));\n\n if (mode && mode === \"grade\") {\n analytics(user, cmid, \"\", true);\n }\n\n emailLink.each(function() {\n let href = $(this).attr('href');\n const $emailLink = $(this);\n let userid = 0;\n if (href) {\n userid = parseInt(new URLSearchParams(href.split('?')[1]).get('userid'));\n if (!userid) {\n $emailLink.closest('tr').find('td:eq(1)').after(\"
\"); // For aligning the table column\n } else {\n\n $('#region-main').on('click', 'table tbody tr td.cell.c1 a', function(e) {\n e.preventDefault();\n const link = e.target.href;\n const url = new URL(link);\n url.searchParams.append('user', userid);\n window.location.href = url.toString();\n });\n\n analytics(userid, cmid, $emailLink, false);\n }\n }\n });\n\n /**\n * Fetches and displays analytics data for lesson submissions\n * @param {number} userid - The ID of the user whose analytics to fetch\n * @param {number} cmid - The course module ID\n * @param {jQuery|string} $emailLink - jQuery object of email link or empty string\n * @param {boolean} grade - Whether this is being called from grade view\n */\n function analytics(userid, cmid, $emailLink, grade) {\n\n let args = {id: userid, modulename: \"lesson\", cmid: cmid};\n let methodname = 'cursive_get_lesson_submission_data';\n let com = getData([{methodname, args}]);\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 let analyticButtonDiv = document.createElement('div');\n let analyticsColumn = document.createElement('td');\n\n if (!hasApiKey) {\n $(analyticButtonDiv).html(replayButton(userid));\n } else {\n analyticButtonDiv.append(analyticButton(data.res.effort_ratio, userid));\n }\n\n analyticButtonDiv.dataset.region = \"analytic-div\" + userid;\n analyticsColumn.append(analyticButtonDiv);\n if (grade) {\n analyticButtonDiv.classList.add('w-100');\n $('#fitem_id_response_editor .felement').prepend(analyticButtonDiv);\n } else {\n $emailLink.closest('tr').find('td:eq(1)').after(analyticsColumn);\n }\n\n\n let myEvents = new AnalyticEvents();\n var context = {\n tabledata: data.res,\n formattime: myEvents.formatedTime(data.res),\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 });\n com[0].fail((error) => {\n window.console.error('Error getting cursive config:', error);\n });\n }\n};"],"names":["scoreSetting","showcomment","hasApiKey","replayInstances","window","video_playback","mid","filepath","replay","Replay","render","then","html","catch","e","console","error","cmid","M","cfg","contextInstanceId","emailLink","headcolumn","url","URL","location","href","mode","searchParams","get","user","analytics","userid","$emailLink","grade","args","id","modulename","com","methodname","done","json","data","JSON","parse","res","filename","analyticButtonDiv","document","createElement","analyticsColumn","append","effort_ratio","dataset","region","classList","add","prepend","closest","find","after","myEvents","AnalyticEvents","context","tabledata","formattime","formatedTime","page","apikey","authIcon","authorshipStatus","first_file","score","createModal","templates","checkDiff","file_id","replyWriting","fail","Str","get_string","strs","each","this","attr","parseInt","URLSearchParams","split","on","preventDefault","link","target","toString"],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"append_lesson_grade_table.min.js","sources":["../src/append_lesson_grade_table.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\r\n//\r\n// Moodle is free software: you can redistribute it and/or modify\r\n// it under the terms of the GNU General Public License as published by\r\n// the Free Software Foundation, either version 3 of the License, or\r\n// (at your option) any later version.\r\n//\r\n// Moodle is distributed in the hope that it will be useful,\r\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\r\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\r\n// GNU General Public License for more details.\r\n//\r\n// You should have received a copy of the GNU General Public License\r\n// along with Moodle. If not, see .\r\n\r\n/**\r\n * Module to append analytics and grade table data for lesson submissions\r\n * Handles the display of analytics, replay functionality and grade information\r\n * for lesson submissions in the Moodle gradebook interface\r\n *\r\n * @module tiny_cursive/append_lesson_grade_table\r\n * @copyright 2025 CTI \r\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\r\n */\r\n\r\nimport Replay from './replay';\r\nimport $ from 'jquery';\r\nimport {call as getData} from 'core/ajax';\r\nimport templates from 'core/templates';\r\nimport AnalyticEvents from './analytic_events';\r\nimport analyticButton from './analytic_button';\r\nimport replayButton from './replay_button';\r\nimport * as Str from 'core/str';\r\n\r\nexport const init = (scoreSetting, showcomment, hasApiKey) => {\r\n const replayInstances = {};\r\n // eslint-disable-next-line camelcase\r\n window.video_playback = function(mid, filepath) {\r\n if (filepath !== '') {\r\n const replay = new Replay(\r\n 'content' + mid,\r\n filepath,\r\n 10,\r\n false,\r\n 'player_' + mid\r\n );\r\n replayInstances[mid] = replay;\r\n } else {\r\n templates.render('tiny_cursive/no_submission').then(html => {\r\n $('#content' + mid).html(html);\r\n return true;\r\n }).catch(e => window.console.error(e));\r\n }\r\n return false;\r\n\r\n };\r\n\r\n\r\n var cmid = M.cfg.contextInstanceId;\r\n var emailLink = $('#page-content div[role=\"main\"] td.lastcol a');\r\n var headcolumn = $('#region-main div[role=\"main\"] table thead tr');\r\n let url = new URL(window.location.href);\r\n let mode = url.searchParams.get('mode');\r\n let user = url.searchParams.get('user');\r\n\r\n Str.get_string('analytics', 'tiny_cursive').then((strs) => {\r\n headcolumn.each(function() {\r\n $(this).find('th:eq(1)').after(`
${strs}
`);\r\n });\r\n return true;\r\n }).catch(e => window.console.error(e));\r\n\r\n if (mode && mode === \"grade\") {\r\n analytics(user, cmid, \"\", true);\r\n }\r\n\r\n emailLink.each(function() {\r\n let href = $(this).attr('href');\r\n const $emailLink = $(this);\r\n let userid = 0;\r\n if (href) {\r\n userid = parseInt(new URLSearchParams(href.split('?')[1]).get('userid'));\r\n if (!userid) {\r\n $emailLink.closest('tr').find('td:eq(1)').after(\"
\"); // For aligning the table column\r\n } else {\r\n\r\n $('#region-main').on('click', 'table tbody tr td.cell.c1 a', function(e) {\r\n e.preventDefault();\r\n const link = e.target.href;\r\n const url = new URL(link);\r\n url.searchParams.append('user', userid);\r\n window.location.href = url.toString();\r\n });\r\n\r\n analytics(userid, cmid, $emailLink, false);\r\n }\r\n }\r\n });\r\n\r\n /**\r\n * Fetches and displays analytics data for lesson submissions\r\n * @param {number} userid - The ID of the user whose analytics to fetch\r\n * @param {number} cmid - The course module ID\r\n * @param {jQuery|string} $emailLink - jQuery object of email link or empty string\r\n * @param {boolean} grade - Whether this is being called from grade view\r\n */\r\n function analytics(userid, cmid, $emailLink, grade) {\r\n\r\n let args = {id: userid, modulename: \"lesson\", cmid: cmid};\r\n let methodname = 'cursive_get_lesson_submission_data';\r\n let com = getData([{methodname, args}]);\r\n com[0].done(function(json) {\r\n var data = JSON.parse(json);\r\n var filepath = '';\r\n if (data.res.filename) {\r\n filepath = data.res.filename;\r\n }\r\n\r\n let analyticButtonDiv = document.createElement('div');\r\n let analyticsColumn = document.createElement('td');\r\n\r\n if (!hasApiKey) {\r\n $(analyticButtonDiv).html(replayButton(userid));\r\n } else {\r\n analyticButtonDiv.append(analyticButton(data.res.effort_ratio, userid));\r\n }\r\n\r\n analyticButtonDiv.dataset.region = \"analytic-div\" + userid;\r\n analyticsColumn.append(analyticButtonDiv);\r\n if (grade) {\r\n analyticButtonDiv.classList.add('w-100');\r\n $('#fitem_id_response_editor .felement').prepend(analyticButtonDiv);\r\n } else {\r\n $emailLink.closest('tr').find('td:eq(1)').after(analyticsColumn);\r\n }\r\n\r\n\r\n let myEvents = new AnalyticEvents();\r\n var context = {\r\n tabledata: data.res,\r\n formattime: myEvents.formatedTime(data.res),\r\n page: scoreSetting,\r\n userid: userid,\r\n apikey: hasApiKey\r\n };\r\n\r\n let authIcon = myEvents.authorshipStatus(data.res.first_file, data.res.score, scoreSetting);\r\n myEvents.createModal(userid, context, '', replayInstances, authIcon);\r\n myEvents.analytics(userid, templates, context, '', replayInstances, authIcon);\r\n myEvents.checkDiff(userid, data.res.file_id, '', replayInstances);\r\n myEvents.replyWriting(userid, filepath, '', replayInstances);\r\n\r\n });\r\n com[0].fail((error) => {\r\n window.console.error('Error getting cursive config:', error);\r\n });\r\n }\r\n};"],"names":["scoreSetting","showcomment","hasApiKey","replayInstances","window","video_playback","mid","filepath","replay","Replay","render","then","html","catch","e","console","error","cmid","M","cfg","contextInstanceId","emailLink","headcolumn","url","URL","location","href","mode","searchParams","get","user","analytics","userid","$emailLink","grade","args","id","modulename","com","methodname","done","json","data","JSON","parse","res","filename","analyticButtonDiv","document","createElement","analyticsColumn","append","effort_ratio","dataset","region","classList","add","prepend","closest","find","after","myEvents","AnalyticEvents","context","tabledata","formattime","formatedTime","page","apikey","authIcon","authorshipStatus","first_file","score","createModal","templates","checkDiff","file_id","replyWriting","fail","Str","get_string","strs","each","this","attr","parseInt","URLSearchParams","split","on","preventDefault","link","target","toString"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/build/append_oublogs_post.min.js.map b/amd/build/append_oublogs_post.min.js.map
index 443769ab..1b309439 100644
--- a/amd/build/append_oublogs_post.min.js.map
+++ b/amd/build/append_oublogs_post.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"append_oublogs_post.min.js","sources":["../src/append_oublogs_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 * This module provides functionality to append blog posts in the OU Blogs plugin for TinyMCE editor\n *\n * @module tiny_cursive/append_oublogs_post\n * @copyright 2025 CTI \n * @author Brain Station 23 \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport $ from 'jquery';\nimport {call as getData} from 'core/ajax';\nimport templates from 'core/templates';\nimport AnalyticEvents from './analytic_events';\nimport analyticButton from './analytic_button';\nimport replayButton from './replay_button';\nimport Replay from './replay';\n\nexport const init = (scoreSetting, comments, hasApiKey) => {\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 const ouBlogPosts = $('.oublog-post');\n const cmid = M.cfg.contextInstanceId;\n ouBlogPosts.each(function() {\n\n var Element = $(this);\n var postedByLink = Element.find('.oublog-postedby a').attr('href');\n var permalink = Element.find('.oublog-post-links a').attr('href');\n var resourceId = parseInt(new URLSearchParams(permalink.split('?')[1]).get('post'));\n var userid = parseInt(new URLSearchParams(postedByLink.split('?')[1]).get('id'));\n var filepath = '';\n\n if (userid && resourceId) {\n let args = {id: userid, resourceid: resourceId, modulename: \"oublog\", cmid: cmid};\n let methodname = 'cursive_get_oublog_submission_data';\n let com = getData([{methodname, args}]);\n com[0].done(function(json) {\n var data = JSON.parse(json);\n\n if (data.res.filename) {\n filepath = data.res.filename;\n }\n\n if (!hasApiKey) {\n Element.find('.oublog-post-links').append(replayButton(userid));\n } else {\n Element.find('.oublog-post-links').append(analyticButton(data.res.effort_ratio, userid));\n }\n\n let myEvents = new AnalyticEvents();\n var context = {\n tabledata: data.res,\n formattime: myEvents.formatedTime(data.res),\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 });\n com[0].fail((error) => {\n window.console.error(error);\n });\n }\n });\n\n};"],"names":["scoreSetting","comments","hasApiKey","replayInstances","window","video_playback","mid","filepath","replay","Replay","render","then","html","catch","e","console","error","ouBlogPosts","cmid","M","cfg","contextInstanceId","each","Element","this","postedByLink","find","attr","permalink","resourceId","parseInt","URLSearchParams","split","get","userid","args","id","resourceid","modulename","methodname","com","done","json","data","JSON","parse","res","filename","append","effort_ratio","myEvents","AnalyticEvents","context","tabledata","formattime","formatedTime","page","apikey","authIcon","authorshipStatus","first_file","score","createModal","analytics","templates","checkDiff","file_id","replyWriting","fail"],"mappings":";;;;;;;;wYAgCoB,CAACA,aAAcC,SAAUC,mBACnCC,gBAAkB,GAExBC,OAAOC,eAAiB,SAASC,IAAKC,aACjB,KAAbA,SAAiB,OACXC,OAAS,IAAIC,gBACf,UAAYH,IACZC,SACA,IACA,EACA,UAAYD,KAEhBH,gBAAgBG,KAAOE,+BAEbE,OAAO,8BAA8BC,MAAKC,2BAC9C,WAAaN,KAAKM,KAAKA,OAClB,KACRC,OAAMC,GAAKV,OAAOW,QAAQC,MAAMF,YAEhC,SAILG,aAAc,mBAAE,gBAChBC,KAAOC,EAAEC,IAAIC,kBACnBJ,YAAYK,MAAK,eAETC,SAAU,mBAAEC,MACZC,aAAeF,QAAQG,KAAK,sBAAsBC,KAAK,QACvDC,UAAYL,QAAQG,KAAK,wBAAwBC,KAAK,QACtDE,WAAaC,SAAS,IAAIC,gBAAgBH,UAAUI,MAAM,KAAK,IAAIC,IAAI,SACvEC,OAASJ,SAAS,IAAIC,gBAAgBN,aAAaO,MAAM,KAAK,IAAIC,IAAI,OACtE1B,SAAW,MAEX2B,QAAUL,WAAY,KAClBM,KAAO,CAACC,GAAIF,OAAQG,WAAYR,WAAYS,WAAY,SAAUpB,KAAMA,MACxEqB,WAAa,qCACbC,KAAM,cAAQ,CAAC,CAACD,WAAAA,WAAYJ,KAAAA,QAChCK,IAAI,GAAGC,MAAK,SAASC,UACbC,KAAOC,KAAKC,MAAMH,MAElBC,KAAKG,IAAIC,WACTxC,SAAWoC,KAAKG,IAAIC,UAGnB7C,UAGDqB,QAAQG,KAAK,sBAAsBsB,QAAO,4BAAeL,KAAKG,IAAIG,aAAcf,SAFhFX,QAAQG,KAAK,sBAAsBsB,QAAO,0BAAad,aAKvDgB,SAAW,IAAIC,6BACfC,QAAU,CACVC,UAAWV,KAAKG,IAChBQ,WAAYJ,SAASK,aAAaZ,KAAKG,KACvCU,KAAMxD,aACNkC,OAAQA,OACRuB,OAAQvD,eAGRwD,SAAWR,SAASS,iBAAiBhB,KAAKG,IAAIc,WAAYjB,KAAKG,IAAIe,MAAO7D,cAC9EkD,SAASY,YAAY5B,OAAQkB,QAAS,GAAIjD,gBAAiBuD,UAC3DR,SAASa,UAAU7B,OAAQ8B,mBAAWZ,QAAS,GAAIjD,gBAAiBuD,UACpER,SAASe,UAAU/B,OAAQS,KAAKG,IAAIoB,QAAS,GAAI/D,iBACjD+C,SAASiB,aAAajC,OAAQ3B,SAAU,GAAIJ,oBAGhDqC,IAAI,GAAG4B,MAAMpD,QACTZ,OAAOW,QAAQC,MAAMA"}
\ No newline at end of file
+{"version":3,"file":"append_oublogs_post.min.js","sources":["../src/append_oublogs_post.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\r\n//\r\n// Moodle is free software: you can redistribute it and/or modify\r\n// it under the terms of the GNU General Public License as published by\r\n// the Free Software Foundation, either version 3 of the License, or\r\n// (at your option) any later version.\r\n//\r\n// Moodle is distributed in the hope that it will be useful,\r\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\r\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\r\n// GNU General Public License for more details.\r\n//\r\n// You should have received a copy of the GNU General Public License\r\n// along with Moodle. If not, see .\r\n\r\n/**\r\n * This module provides functionality to append blog posts in the OU Blogs plugin for TinyMCE editor\r\n *\r\n * @module tiny_cursive/append_oublogs_post\r\n * @copyright 2025 CTI \r\n * @author Brain Station 23 \r\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\r\n */\r\n\r\nimport $ from 'jquery';\r\nimport {call as getData} from 'core/ajax';\r\nimport templates from 'core/templates';\r\nimport AnalyticEvents from './analytic_events';\r\nimport analyticButton from './analytic_button';\r\nimport replayButton from './replay_button';\r\nimport Replay from './replay';\r\n\r\nexport const init = (scoreSetting, comments, hasApiKey) => {\r\n const replayInstances = {};\r\n // eslint-disable-next-line camelcase\r\n window.video_playback = function(mid, filepath) {\r\n if (filepath !== '') {\r\n const replay = new Replay(\r\n 'content' + mid,\r\n filepath,\r\n 10,\r\n false,\r\n 'player_' + mid\r\n );\r\n replayInstances[mid] = replay;\r\n } else {\r\n templates.render('tiny_cursive/no_submission').then(html => {\r\n $('#content' + mid).html(html);\r\n return true;\r\n }).catch(e => window.console.error(e));\r\n }\r\n return false;\r\n\r\n };\r\n\r\n const ouBlogPosts = $('.oublog-post');\r\n const cmid = M.cfg.contextInstanceId;\r\n ouBlogPosts.each(function() {\r\n\r\n var Element = $(this);\r\n var postedByLink = Element.find('.oublog-postedby a').attr('href');\r\n var permalink = Element.find('.oublog-post-links a').attr('href');\r\n var resourceId = parseInt(new URLSearchParams(permalink.split('?')[1]).get('post'));\r\n var userid = parseInt(new URLSearchParams(postedByLink.split('?')[1]).get('id'));\r\n var filepath = '';\r\n\r\n if (userid && resourceId) {\r\n let args = {id: userid, resourceid: resourceId, modulename: \"oublog\", cmid: cmid};\r\n let methodname = 'cursive_get_oublog_submission_data';\r\n let com = getData([{methodname, args}]);\r\n com[0].done(function(json) {\r\n var data = JSON.parse(json);\r\n\r\n if (data.res.filename) {\r\n filepath = data.res.filename;\r\n }\r\n\r\n if (!hasApiKey) {\r\n Element.find('.oublog-post-links').append(replayButton(userid));\r\n } else {\r\n Element.find('.oublog-post-links').append(analyticButton(data.res.effort_ratio, userid));\r\n }\r\n\r\n let myEvents = new AnalyticEvents();\r\n var context = {\r\n tabledata: data.res,\r\n formattime: myEvents.formatedTime(data.res),\r\n page: scoreSetting,\r\n userid: userid,\r\n apikey: hasApiKey\r\n };\r\n\r\n let authIcon = myEvents.authorshipStatus(data.res.first_file, data.res.score, scoreSetting);\r\n myEvents.createModal(userid, context, '', replayInstances, authIcon);\r\n myEvents.analytics(userid, templates, context, '', replayInstances, authIcon);\r\n myEvents.checkDiff(userid, data.res.file_id, '', replayInstances);\r\n myEvents.replyWriting(userid, filepath, '', replayInstances);\r\n\r\n });\r\n com[0].fail((error) => {\r\n window.console.error(error);\r\n });\r\n }\r\n });\r\n\r\n};"],"names":["scoreSetting","comments","hasApiKey","replayInstances","window","video_playback","mid","filepath","replay","Replay","render","then","html","catch","e","console","error","ouBlogPosts","cmid","M","cfg","contextInstanceId","each","Element","this","postedByLink","find","attr","permalink","resourceId","parseInt","URLSearchParams","split","get","userid","args","id","resourceid","modulename","methodname","com","done","json","data","JSON","parse","res","filename","append","effort_ratio","myEvents","AnalyticEvents","context","tabledata","formattime","formatedTime","page","apikey","authIcon","authorshipStatus","first_file","score","createModal","analytics","templates","checkDiff","file_id","replyWriting","fail"],"mappings":";;;;;;;;wYAgCoB,CAACA,aAAcC,SAAUC,mBACnCC,gBAAkB,GAExBC,OAAOC,eAAiB,SAASC,IAAKC,aACjB,KAAbA,SAAiB,OACXC,OAAS,IAAIC,gBACf,UAAYH,IACZC,SACA,IACA,EACA,UAAYD,KAEhBH,gBAAgBG,KAAOE,+BAEbE,OAAO,8BAA8BC,MAAKC,2BAC9C,WAAaN,KAAKM,KAAKA,OAClB,KACRC,OAAMC,GAAKV,OAAOW,QAAQC,MAAMF,YAEhC,SAILG,aAAc,mBAAE,gBAChBC,KAAOC,EAAEC,IAAIC,kBACnBJ,YAAYK,MAAK,eAETC,SAAU,mBAAEC,MACZC,aAAeF,QAAQG,KAAK,sBAAsBC,KAAK,QACvDC,UAAYL,QAAQG,KAAK,wBAAwBC,KAAK,QACtDE,WAAaC,SAAS,IAAIC,gBAAgBH,UAAUI,MAAM,KAAK,IAAIC,IAAI,SACvEC,OAASJ,SAAS,IAAIC,gBAAgBN,aAAaO,MAAM,KAAK,IAAIC,IAAI,OACtE1B,SAAW,MAEX2B,QAAUL,WAAY,KAClBM,KAAO,CAACC,GAAIF,OAAQG,WAAYR,WAAYS,WAAY,SAAUpB,KAAMA,MACxEqB,WAAa,qCACbC,KAAM,cAAQ,CAAC,CAACD,WAAAA,WAAYJ,KAAAA,QAChCK,IAAI,GAAGC,MAAK,SAASC,UACbC,KAAOC,KAAKC,MAAMH,MAElBC,KAAKG,IAAIC,WACTxC,SAAWoC,KAAKG,IAAIC,UAGnB7C,UAGDqB,QAAQG,KAAK,sBAAsBsB,QAAO,4BAAeL,KAAKG,IAAIG,aAAcf,SAFhFX,QAAQG,KAAK,sBAAsBsB,QAAO,0BAAad,aAKvDgB,SAAW,IAAIC,6BACfC,QAAU,CACVC,UAAWV,KAAKG,IAChBQ,WAAYJ,SAASK,aAAaZ,KAAKG,KACvCU,KAAMxD,aACNkC,OAAQA,OACRuB,OAAQvD,eAGRwD,SAAWR,SAASS,iBAAiBhB,KAAKG,IAAIc,WAAYjB,KAAKG,IAAIe,MAAO7D,cAC9EkD,SAASY,YAAY5B,OAAQkB,QAAS,GAAIjD,gBAAiBuD,UAC3DR,SAASa,UAAU7B,OAAQ8B,mBAAWZ,QAAS,GAAIjD,gBAAiBuD,UACpER,SAASe,UAAU/B,OAAQS,KAAKG,IAAIoB,QAAS,GAAI/D,iBACjD+C,SAASiB,aAAajC,OAAQ3B,SAAU,GAAIJ,oBAGhDqC,IAAI,GAAG4B,MAAMpD,QACTZ,OAAOW,QAAQC,MAAMA"}
\ No newline at end of file
diff --git a/amd/build/append_participants_table.min.js.map b/amd/build/append_participants_table.min.js.map
index f5630dc2..d0197189 100644
--- a/amd/build/append_participants_table.min.js.map
+++ b/amd/build/append_participants_table.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"append_participants_table.min.js","sources":["../src/append_participants_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_participants_table\n * @category TinyMCE Editor\n * @copyright CTI \n * @author kuldeep singh \n */\n\ndefine([\"jquery\", \"core/config\", \"core/str\"], function($, mdlcfg, Str) {\n var usersTable = {\n init: async function(page) {\n await usersTable.appendTable(page);\n },\n appendTable: async function() {\n $(document).ready(async function($) {\n // Get the first row in the table header\n let hTr = $('thead').find('tr').get()[0];\n let courseid = M.cfg.courseId;\n\n // Fetch string for stats header\n let statsString = await Str.get_string('stats', 'tiny_cursive');\n\n // Add the stats header if it doesn't already exist\n if (!$(hTr).find('#stats').length) {\n $(hTr).find('th').eq(6).after('
' + statsString + '
');\n }\n\n // Iterate over each row in the table body\n $('tbody').find(\"tr\").get().forEach(function(tr) {\n let tdUser = $(tr).find(\"td\").get()[0];\n let userid = $(tdUser).find(\"input\").get()[0]?.id;\n userid = userid?.slice(4); // Extract userid from input id\n\n if (userid) {\n // Avoid duplicating the icon by checking if the icon is already added\n if (!$(tr).find('td').eq(6).find('i').length) {\n let color = 'font-size:24px;color:black;text-decoration:none';\n let link = mdlcfg.wwwroot + \"/lib/editor/tiny/plugins/cursive/writing_report.php?userid=\"\n + userid + \"&courseid=\" + courseid;\n let icon = 'fa fa-area-chart';\n\n // Add the icon link to the 6th column\n let thunderIcon = '
';\n $(tr).find('td').eq(5).after(thunderIcon); // Insert after the 5th column\n }\n }\n });\n\n // Add event listener for page change or other events triggering table update\n $(\".page-item, .header\").on('click', function() {\n setTimeout(() => {\n // Prevent multiple initializations by checking if already initialized\n if (!$('#stats').length) {\n usersTable.init(); // Initialize the table if needed\n }\n }, 1800); // Slight delay to ensure the DOM is fully updated\n });\n });\n }\n };\n return usersTable;\n});\n"],"names":["define","$","mdlcfg","Str","usersTable","init","async","page","appendTable","document","ready","hTr","find","get","courseid","M","cfg","courseId","statsString","get_string","length","eq","after","forEach","tr","tdUser","userid","_$$find$get$","id","_userid","slice","color","thunderIcon","wwwroot","on","setTimeout"],"mappings":"AAsBAA,gDAAO,CAAC,SAAU,cAAe,aAAa,SAASC,EAAGC,OAAQC,SAC1DC,WAAa,CACbC,KAAMC,eAAeC,YACXH,WAAWI,YAAYD,OAEjCC,YAAaF,iBACTL,EAAEQ,UAAUC,OAAMJ,eAAeL,OAEzBU,IAAMV,EAAE,SAASW,KAAK,MAAMC,MAAM,GAClCC,SAAWC,EAAEC,IAAIC,SAGjBC,kBAAoBf,IAAIgB,WAAW,QAAS,gBAG3ClB,EAAEU,KAAKC,KAAK,UAAUQ,QACvBnB,EAAEU,KAAKC,KAAK,MAAMS,GAAG,GAAGC,MAAM,oCAAsCJ,YAAc,SAItFjB,EAAE,SAASW,KAAK,MAAMC,MAAMU,SAAQ,SAASC,iCACrCC,OAASxB,EAAEuB,IAAIZ,KAAK,MAAMC,MAAM,GAChCa,4BAASzB,EAAEwB,QAAQb,KAAK,SAASC,MAAM,kCAA9Bc,aAAkCC,MAC/CF,uBAASA,iCAAAG,QAAQC,MAAM,GAEnBJ,SAEKzB,EAAEuB,IAAIZ,KAAK,MAAMS,GAAG,GAAGT,KAAK,KAAKQ,OAAQ,KACtCW,MAAQ,kDAMRC,YAAc,iBALP9B,OAAO+B,QAAU,8DACtBP,OAAS,aAAeZ,UAIa,aAAeY,OAAxC,cAHP,mBAIe,+BAAiCK,MAAQ,kBACnE9B,EAAEuB,IAAIZ,KAAK,MAAMS,GAAG,GAAGC,MAAMU,iBAMzC/B,EAAE,uBAAuBiC,GAAG,SAAS,WACjCC,YAAW,KAEFlC,EAAE,UAAUmB,QACbhB,WAAWC,SAEhB,qBAKZD"}
\ No newline at end of file
+{"version":3,"file":"append_participants_table.min.js","sources":["../src/append_participants_table.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\r\n//\r\n// Moodle is free software: you can redistribute it and/or modify\r\n// it under the terms of the GNU General Public License as published by\r\n// the Free Software Foundation, either version 3 of the License, or\r\n// (at your option) any later version.\r\n//\r\n// Moodle is distributed in the hope that it will be useful,\r\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\r\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\r\n// GNU General Public License for more details.\r\n//\r\n// You should have received a copy of the GNU General Public License\r\n// along with Moodle. If not, see .\r\n\r\n/**\r\n * @module tiny_cursive/append_participants_table\r\n * @category TinyMCE Editor\r\n * @copyright CTI \r\n * @author kuldeep singh \r\n */\r\n\r\ndefine([\"jquery\", \"core/config\", \"core/str\"], function($, mdlcfg, Str) {\r\n var usersTable = {\r\n init: async function(page) {\r\n await usersTable.appendTable(page);\r\n },\r\n appendTable: async function() {\r\n $(document).ready(async function($) {\r\n // Get the first row in the table header\r\n let hTr = $('thead').find('tr').get()[0];\r\n let courseid = M.cfg.courseId;\r\n\r\n // Fetch string for stats header\r\n let statsString = await Str.get_string('stats', 'tiny_cursive');\r\n\r\n // Add the stats header if it doesn't already exist\r\n if (!$(hTr).find('#stats').length) {\r\n $(hTr).find('th').eq(6).after('
' + statsString + '
');\r\n }\r\n\r\n // Iterate over each row in the table body\r\n $('tbody').find(\"tr\").get().forEach(function(tr) {\r\n let tdUser = $(tr).find(\"td\").get()[0];\r\n let userid = $(tdUser).find(\"input\").get()[0]?.id;\r\n userid = userid?.slice(4); // Extract userid from input id\r\n\r\n if (userid) {\r\n // Avoid duplicating the icon by checking if the icon is already added\r\n if (!$(tr).find('td').eq(6).find('i').length) {\r\n let color = 'font-size:24px;color:black;text-decoration:none';\r\n let link = mdlcfg.wwwroot + \"/lib/editor/tiny/plugins/cursive/writing_report.php?userid=\"\r\n + userid + \"&courseid=\" + courseid;\r\n let icon = 'fa fa-area-chart';\r\n\r\n // Add the icon link to the 6th column\r\n let thunderIcon = '
';\r\n $(tr).find('td').eq(5).after(thunderIcon); // Insert after the 5th column\r\n }\r\n }\r\n });\r\n\r\n // Add event listener for page change or other events triggering table update\r\n $(\".page-item, .header\").on('click', function() {\r\n setTimeout(() => {\r\n // Prevent multiple initializations by checking if already initialized\r\n if (!$('#stats').length) {\r\n usersTable.init(); // Initialize the table if needed\r\n }\r\n }, 1800); // Slight delay to ensure the DOM is fully updated\r\n });\r\n });\r\n }\r\n };\r\n return usersTable;\r\n});\r\n"],"names":["define","$","mdlcfg","Str","usersTable","init","async","page","appendTable","document","ready","hTr","find","get","courseid","M","cfg","courseId","statsString","get_string","length","eq","after","forEach","tr","tdUser","userid","_$$find$get$","id","_userid","slice","color","thunderIcon","wwwroot","on","setTimeout"],"mappings":"AAsBAA,gDAAO,CAAC,SAAU,cAAe,aAAa,SAASC,EAAGC,OAAQC,SAC1DC,WAAa,CACbC,KAAMC,eAAeC,YACXH,WAAWI,YAAYD,OAEjCC,YAAaF,iBACTL,EAAEQ,UAAUC,OAAMJ,eAAeL,OAEzBU,IAAMV,EAAE,SAASW,KAAK,MAAMC,MAAM,GAClCC,SAAWC,EAAEC,IAAIC,SAGjBC,kBAAoBf,IAAIgB,WAAW,QAAS,gBAG3ClB,EAAEU,KAAKC,KAAK,UAAUQ,QACvBnB,EAAEU,KAAKC,KAAK,MAAMS,GAAG,GAAGC,MAAM,oCAAsCJ,YAAc,SAItFjB,EAAE,SAASW,KAAK,MAAMC,MAAMU,SAAQ,SAASC,iCACrCC,OAASxB,EAAEuB,IAAIZ,KAAK,MAAMC,MAAM,GAChCa,4BAASzB,EAAEwB,QAAQb,KAAK,SAASC,MAAM,kCAA9Bc,aAAkCC,MAC/CF,uBAASA,iCAAAG,QAAQC,MAAM,GAEnBJ,SAEKzB,EAAEuB,IAAIZ,KAAK,MAAMS,GAAG,GAAGT,KAAK,KAAKQ,OAAQ,KACtCW,MAAQ,kDAMRC,YAAc,iBALP9B,OAAO+B,QAAU,8DACtBP,OAAS,aAAeZ,UAIa,aAAeY,OAAxC,cAHP,mBAIe,+BAAiCK,MAAQ,kBACnE9B,EAAEuB,IAAIZ,KAAK,MAAMS,GAAG,GAAGC,MAAMU,iBAMzC/B,EAAE,uBAAuBiC,GAAG,SAAS,WACjCC,YAAW,KAEFlC,EAAE,UAAUmB,QACbhB,WAAWC,SAEhB,qBAKZD"}
\ No newline at end of file
diff --git a/amd/build/append_pdfannotator.min.js.map b/amd/build/append_pdfannotator.min.js.map
index efbbc64c..4d8a05e5 100644
--- a/amd/build/append_pdfannotator.min.js.map
+++ b/amd/build/append_pdfannotator.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"append_pdfannotator.min.js","sources":["../src/append_pdfannotator.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 for handling PDF annotator functionality,\n *\n * @module tiny_cursive/append_pdfannotator\n * @copyright 2025 Cursive Technology, Inc. \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {call} from 'core/ajax';\nimport analyticButton from 'tiny_cursive/analytic_button';\nimport replayButton from 'tiny_cursive/replay_button';\nimport AnalyticEvents from 'tiny_cursive/analytic_events';\nimport templates from 'core/templates';\nimport Replay from 'tiny_cursive/replay';\nexport const init = (scoreSetting, comments, hasApiKey, userid) => {\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 document.getElementById('content' + mid).innerHTML = html;\n return true;\n }).catch(e => window.console.error(e));\n }\n return false;\n };\n\n let container = document.querySelector('.comment-list-container');\n const overviewTable = document.querySelector('table[id^=\"mod-pdfannotator-\"]');\n\n document.addEventListener('click', handleSubmit);\n const moduleName = document.body.id.split('-')[2];\n var pendingSubmit = false;\n var buttonElement = \"\";\n\n if (container) {\n const observer = new MutationObserver(() => {\n if (container?.lastChild?.id) {\n extractResourceId(container.lastChild.id);\n }\n });\n\n observer.observe(container, {\n subtree: true,\n childList: true\n });\n }\n\n if (overviewTable) {\n let newChild = document.createElement('th');\n newChild.textContent = 'Analytics';\n let header = overviewTable.querySelector('thead>tr>th:first-child');\n header.insertAdjacentElement('afterend', newChild);\n setReplayButton(overviewTable);\n }\n\n /**\n * Sets up replay buttons and analytics for each row in the overview table\n * @param {HTMLTableElement} overviewTable - The table element containing the overview data\n * @description This function:\n * 1. Gets all rows from the table\n * 2. For each row:\n * - Extracts comment ID and user ID from relevant links\n * - Adds analytics column with replay/analytics buttons\n * - Sets up cursive analytics functionality\n */\n function setReplayButton(overviewTable) {\n const rows = overviewTable.querySelectorAll('tbody > tr');\n const action = new URL(window.location.href).searchParams.get('action');\n\n rows.forEach(row => {\n const cols = {\n col1: row.querySelector('td:nth-child(1)'),\n col2: row.querySelector('td:nth-child(2)'),\n col3: row.querySelector('td:nth-child(3)')\n };\n\n const links = {\n link1: cols.col1?.querySelector('a'),\n link2: cols.col2?.querySelector('a'),\n link3: cols.col3?.querySelector('a')\n };\n\n // Extract comment ID safely\n const commentId = links.link1?.href ?\n new URL(links.link1.href).searchParams.get('commid') : null;\n const cmid = links.link1?.href ?\n new URL(links.link1.href).searchParams.get('id') : M.cfg.contextInstanceId;\n // Extract user ID based on action\n let userId = null;\n let userLink = null;\n\n switch (action) {\n case 'overviewquestions':\n userLink = links.link2;\n break;\n case 'overviewanswers':\n userLink = links.link3;\n break;\n default:\n userId = userid;\n }\n\n if (userLink?.href) {\n try {\n userId = new URL(userLink.href).searchParams.get('id');\n } catch (e) {\n window.console.warn('Error parsing user URL:', e);\n }\n }\n\n getCursiveAnalytics(userId, commentId, cmid, cols.col1);\n });\n }\n\n /**\n * Handles the submission and cancellation of comments\n * @param {Event} e - The click event object\n * @description When comment is submitted or cancelled:\n * - Removes 'isEditing' flag from localStorage\n * - Sets pendingSubmit flag appropriately (true for submit, false for cancel)\n */\n function handleSubmit(e) {\n if (e.target.id === 'commentSubmit') {\n localStorage.removeItem('isEditing');\n buttonElement = e.target.value;\n pendingSubmit = true;\n }\n if (e.target.id === 'commentCancel') {\n localStorage.removeItem('isEditing');\n pendingSubmit = false;\n }\n }\n\n const updateEntries = async(methodname, args) => {\n try {\n const response = await call([{\n methodname,\n args,\n }])[0];\n return response;\n } catch (error) {\n window.console.error('updating Entries:', error);\n throw error;\n }\n };\n\n /**\n * Extracts the resource ID from a comment ID and updates entries if submission is pending\n * @param {string} id - The ID string to extract resource ID from, expected format: 'prefix_number'\n * @description This function:\n * 1. Parses the resource ID from the given ID string\n * 2. If resource ID exists and there's a pending submission:\n * - Resets the pending submission flag\n * - Constructs arguments with context info\n * - Calls updateEntries to process the PDF annotation\n */\n function extractResourceId(id) {\n\n // Prevent updating ID while editing a existing entry.\n if (buttonElement === 'Save') {\n\n pendingSubmit = false;\n return;\n }\n\n let resourceId = parseInt(id?.split('_')[1]);\n if (resourceId && pendingSubmit) {\n pendingSubmit = false;\n let args = {\n cmid: M.cfg.contextInstanceId,\n userid: M.cfg.userId ?? 0,\n courseid: M.cfg.courseId,\n modulename: moduleName,\n resourceid: resourceId\n };\n updateEntries('cursive_update_pdf_annote_id', args);\n }\n }\n\n /**\n * Retrieves and displays cursive analytics for a given resource\n * @param {number} userid - The ID of the user\n * @param {number} resourceid - The ID of the resource to get analytics for\n * @param {number} cmid - The course module ID\n * @param {HTMLElement} place - The DOM element where analytics should be placed\n * @description This function:\n * 1. Makes an AJAX call to get forum comment data\n * 2. Creates and inserts analytics/replay buttons\n * 3. Sets up analytics events and modal functionality\n * 4. Handles both API key and non-API key scenarios\n */\n function getCursiveAnalytics(userid, resourceid, cmid, place) {\n let args = {id: resourceid, modulename: \"pdfannotator\", cmid: cmid};\n let methodname = 'cursive_get_forum_comment_link';\n let com = 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\n let analyticButtonDiv = document.createElement('div');\n let analyticsColumn = document.createElement('td');\n\n if (!hasApiKey) {\n analyticButtonDiv.append(replayButton(resourceid));\n } else {\n analyticButtonDiv.append(analyticButton(data.data.effort_ratio, resourceid));\n }\n\n analyticButtonDiv.dataset.region = \"analytic-div\" + userid;\n analyticsColumn.append(analyticButtonDiv);\n place.insertAdjacentElement('afterend', analyticsColumn);\n\n let myEvents = new AnalyticEvents();\n var context = {\n tabledata: data.data,\n formattime: myEvents.formatedTime(data.data),\n page: scoreSetting,\n userid: resourceid,\n apikey: hasApiKey\n };\n\n let authIcon = myEvents.authorshipStatus(data.data.first_file, data.data.score, scoreSetting);\n myEvents.createModal(resourceid, context, '', replayInstances, authIcon);\n myEvents.analytics(resourceid, templates, context, '', replayInstances, authIcon);\n myEvents.checkDiff(resourceid, data.data.file_id, '', replayInstances);\n myEvents.replyWriting(resourceid, filepath, '', replayInstances);\n\n });\n com[0].fail((error) => {\n window.console.error('Error getting cursive config:', error);\n });\n }\n};"],"names":["scoreSetting","comments","hasApiKey","userid","replayInstances","window","video_playback","mid","filepath","replay","Replay","render","then","html","document","getElementById","innerHTML","catch","e","console","error","container","querySelector","overviewTable","addEventListener","target","id","localStorage","removeItem","buttonElement","value","pendingSubmit","moduleName","body","split","MutationObserver","lastChild","_container$lastChild","resourceId","parseInt","async","methodname","args","updateEntries","cmid","M","cfg","contextInstanceId","userId","courseid","courseId","modulename","resourceid","extractResourceId","observe","subtree","childList","newChild","createElement","textContent","insertAdjacentElement","rows","querySelectorAll","action","URL","location","href","searchParams","get","forEach","row","cols","col1","col2","col3","links","link1","_cols$col","link2","_cols$col2","link3","_cols$col3","commentId","userLink","_userLink","warn","place","com","done","json","data","JSON","parse","filename","analyticButtonDiv","analyticsColumn","append","effort_ratio","dataset","region","myEvents","AnalyticEvents","context","tabledata","formattime","formatedTime","page","apikey","authIcon","authorshipStatus","first_file","score","createModal","analytics","templates","checkDiff","file_id","replyWriting","fail","getCursiveAnalytics","setReplayButton"],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"append_pdfannotator.min.js","sources":["../src/append_pdfannotator.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\r\n//\r\n// Moodle is free software: you can redistribute it and/or modify\r\n// it under the terms of the GNU General Public License as published by\r\n// the Free Software Foundation, either version 3 of the License, or\r\n// (at your option) any later version.\r\n//\r\n// Moodle is distributed in the hope that it will be useful,\r\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\r\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\r\n// GNU General Public License for more details.\r\n//\r\n// You should have received a copy of the GNU General Public License\r\n// along with Moodle. If not, see .\r\n\r\n/**\r\n * Module for handling PDF annotator functionality,\r\n *\r\n * @module tiny_cursive/append_pdfannotator\r\n * @copyright 2025 Cursive Technology, Inc. \r\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\r\n */\r\n\r\nimport {call} from 'core/ajax';\r\nimport analyticButton from 'tiny_cursive/analytic_button';\r\nimport replayButton from 'tiny_cursive/replay_button';\r\nimport AnalyticEvents from 'tiny_cursive/analytic_events';\r\nimport templates from 'core/templates';\r\nimport Replay from 'tiny_cursive/replay';\r\nexport const init = (scoreSetting, comments, hasApiKey, userid) => {\r\n const replayInstances = {};\r\n // eslint-disable-next-line camelcase\r\n window.video_playback = function(mid, filepath) {\r\n if (filepath !== '') {\r\n const replay = new Replay(\r\n 'content' + mid,\r\n filepath,\r\n 10,\r\n false,\r\n 'player_' + mid\r\n );\r\n replayInstances[mid] = replay;\r\n } else {\r\n templates.render('tiny_cursive/no_submission').then(html => {\r\n document.getElementById('content' + mid).innerHTML = html;\r\n return true;\r\n }).catch(e => window.console.error(e));\r\n }\r\n return false;\r\n };\r\n\r\n let container = document.querySelector('.comment-list-container');\r\n const overviewTable = document.querySelector('table[id^=\"mod-pdfannotator-\"]');\r\n\r\n document.addEventListener('click', handleSubmit);\r\n const moduleName = document.body.id.split('-')[2];\r\n var pendingSubmit = false;\r\n var buttonElement = \"\";\r\n\r\n if (container) {\r\n const observer = new MutationObserver(() => {\r\n if (container?.lastChild?.id) {\r\n extractResourceId(container.lastChild.id);\r\n }\r\n });\r\n\r\n observer.observe(container, {\r\n subtree: true,\r\n childList: true\r\n });\r\n }\r\n\r\n if (overviewTable) {\r\n let newChild = document.createElement('th');\r\n newChild.textContent = 'Analytics';\r\n let header = overviewTable.querySelector('thead>tr>th:first-child');\r\n header.insertAdjacentElement('afterend', newChild);\r\n setReplayButton(overviewTable);\r\n }\r\n\r\n /**\r\n * Sets up replay buttons and analytics for each row in the overview table\r\n * @param {HTMLTableElement} overviewTable - The table element containing the overview data\r\n * @description This function:\r\n * 1. Gets all rows from the table\r\n * 2. For each row:\r\n * - Extracts comment ID and user ID from relevant links\r\n * - Adds analytics column with replay/analytics buttons\r\n * - Sets up cursive analytics functionality\r\n */\r\n function setReplayButton(overviewTable) {\r\n const rows = overviewTable.querySelectorAll('tbody > tr');\r\n const action = new URL(window.location.href).searchParams.get('action');\r\n\r\n rows.forEach(row => {\r\n const cols = {\r\n col1: row.querySelector('td:nth-child(1)'),\r\n col2: row.querySelector('td:nth-child(2)'),\r\n col3: row.querySelector('td:nth-child(3)')\r\n };\r\n\r\n const links = {\r\n link1: cols.col1?.querySelector('a'),\r\n link2: cols.col2?.querySelector('a'),\r\n link3: cols.col3?.querySelector('a')\r\n };\r\n\r\n // Extract comment ID safely\r\n const commentId = links.link1?.href ?\r\n new URL(links.link1.href).searchParams.get('commid') : null;\r\n const cmid = links.link1?.href ?\r\n new URL(links.link1.href).searchParams.get('id') : M.cfg.contextInstanceId;\r\n // Extract user ID based on action\r\n let userId = null;\r\n let userLink = null;\r\n\r\n switch (action) {\r\n case 'overviewquestions':\r\n userLink = links.link2;\r\n break;\r\n case 'overviewanswers':\r\n userLink = links.link3;\r\n break;\r\n default:\r\n userId = userid;\r\n }\r\n\r\n if (userLink?.href) {\r\n try {\r\n userId = new URL(userLink.href).searchParams.get('id');\r\n } catch (e) {\r\n window.console.warn('Error parsing user URL:', e);\r\n }\r\n }\r\n\r\n getCursiveAnalytics(userId, commentId, cmid, cols.col1);\r\n });\r\n }\r\n\r\n /**\r\n * Handles the submission and cancellation of comments\r\n * @param {Event} e - The click event object\r\n * @description When comment is submitted or cancelled:\r\n * - Removes 'isEditing' flag from localStorage\r\n * - Sets pendingSubmit flag appropriately (true for submit, false for cancel)\r\n */\r\n function handleSubmit(e) {\r\n if (e.target.id === 'commentSubmit') {\r\n localStorage.removeItem('isEditing');\r\n buttonElement = e.target.value;\r\n pendingSubmit = true;\r\n }\r\n if (e.target.id === 'commentCancel') {\r\n localStorage.removeItem('isEditing');\r\n pendingSubmit = false;\r\n }\r\n }\r\n\r\n const updateEntries = async(methodname, args) => {\r\n try {\r\n const response = await call([{\r\n methodname,\r\n args,\r\n }])[0];\r\n return response;\r\n } catch (error) {\r\n window.console.error('updating Entries:', error);\r\n throw error;\r\n }\r\n };\r\n\r\n /**\r\n * Extracts the resource ID from a comment ID and updates entries if submission is pending\r\n * @param {string} id - The ID string to extract resource ID from, expected format: 'prefix_number'\r\n * @description This function:\r\n * 1. Parses the resource ID from the given ID string\r\n * 2. If resource ID exists and there's a pending submission:\r\n * - Resets the pending submission flag\r\n * - Constructs arguments with context info\r\n * - Calls updateEntries to process the PDF annotation\r\n */\r\n function extractResourceId(id) {\r\n\r\n // Prevent updating ID while editing a existing entry.\r\n if (buttonElement === 'Save') {\r\n\r\n pendingSubmit = false;\r\n return;\r\n }\r\n\r\n let resourceId = parseInt(id?.split('_')[1]);\r\n if (resourceId && pendingSubmit) {\r\n pendingSubmit = false;\r\n let args = {\r\n cmid: M.cfg.contextInstanceId,\r\n userid: M.cfg.userId ?? 0,\r\n courseid: M.cfg.courseId,\r\n modulename: moduleName,\r\n resourceid: resourceId\r\n };\r\n updateEntries('cursive_update_pdf_annote_id', args);\r\n }\r\n }\r\n\r\n /**\r\n * Retrieves and displays cursive analytics for a given resource\r\n * @param {number} userid - The ID of the user\r\n * @param {number} resourceid - The ID of the resource to get analytics for\r\n * @param {number} cmid - The course module ID\r\n * @param {HTMLElement} place - The DOM element where analytics should be placed\r\n * @description This function:\r\n * 1. Makes an AJAX call to get forum comment data\r\n * 2. Creates and inserts analytics/replay buttons\r\n * 3. Sets up analytics events and modal functionality\r\n * 4. Handles both API key and non-API key scenarios\r\n */\r\n function getCursiveAnalytics(userid, resourceid, cmid, place) {\r\n let args = {id: resourceid, modulename: \"pdfannotator\", cmid: cmid};\r\n let methodname = 'cursive_get_forum_comment_link';\r\n let com = call([{methodname, args}]);\r\n com[0].done(function(json) {\r\n var data = JSON.parse(json);\r\n\r\n var filepath = '';\r\n if (data.data.filename) {\r\n filepath = data.data.filename;\r\n }\r\n\r\n let analyticButtonDiv = document.createElement('div');\r\n let analyticsColumn = document.createElement('td');\r\n\r\n if (!hasApiKey) {\r\n analyticButtonDiv.append(replayButton(resourceid));\r\n } else {\r\n analyticButtonDiv.append(analyticButton(data.data.effort_ratio, resourceid));\r\n }\r\n\r\n analyticButtonDiv.dataset.region = \"analytic-div\" + userid;\r\n analyticsColumn.append(analyticButtonDiv);\r\n place.insertAdjacentElement('afterend', analyticsColumn);\r\n\r\n let myEvents = new AnalyticEvents();\r\n var context = {\r\n tabledata: data.data,\r\n formattime: myEvents.formatedTime(data.data),\r\n page: scoreSetting,\r\n userid: resourceid,\r\n apikey: hasApiKey\r\n };\r\n\r\n let authIcon = myEvents.authorshipStatus(data.data.first_file, data.data.score, scoreSetting);\r\n myEvents.createModal(resourceid, context, '', replayInstances, authIcon);\r\n myEvents.analytics(resourceid, templates, context, '', replayInstances, authIcon);\r\n myEvents.checkDiff(resourceid, data.data.file_id, '', replayInstances);\r\n myEvents.replyWriting(resourceid, filepath, '', replayInstances);\r\n\r\n });\r\n com[0].fail((error) => {\r\n window.console.error('Error getting cursive config:', error);\r\n });\r\n }\r\n};"],"names":["scoreSetting","comments","hasApiKey","userid","replayInstances","window","video_playback","mid","filepath","replay","Replay","render","then","html","document","getElementById","innerHTML","catch","e","console","error","container","querySelector","overviewTable","addEventListener","target","id","localStorage","removeItem","buttonElement","value","pendingSubmit","moduleName","body","split","MutationObserver","lastChild","_container$lastChild","resourceId","parseInt","async","methodname","args","updateEntries","cmid","M","cfg","contextInstanceId","userId","courseid","courseId","modulename","resourceid","extractResourceId","observe","subtree","childList","newChild","createElement","textContent","insertAdjacentElement","rows","querySelectorAll","action","URL","location","href","searchParams","get","forEach","row","cols","col1","col2","col3","links","link1","_cols$col","link2","_cols$col2","link3","_cols$col3","commentId","userLink","_userLink","warn","place","com","done","json","data","JSON","parse","filename","analyticButtonDiv","analyticsColumn","append","effort_ratio","dataset","region","myEvents","AnalyticEvents","context","tabledata","formattime","formatedTime","page","apikey","authIcon","authorshipStatus","first_file","score","createModal","analytics","templates","checkDiff","file_id","replyWriting","fail","getCursiveAnalytics","setReplayButton"],"mappings":"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"}
\ 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..5b06ff1f 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 + analyticString + '
' +\n '
');\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/\r\n//\r\n// Moodle is free software: you can redistribute it and/or modify\r\n// it under the terms of the GNU General Public License as published by\r\n// the Free Software Foundation, either version 3 of the License, or\r\n// (at your option) any later version.\r\n//\r\n// Moodle is distributed in the hope that it will be useful,\r\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\r\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\r\n// GNU General Public License for more details.\r\n//\r\n// You should have received a copy of the GNU General Public License\r\n// along with Moodle. If not, see .\r\n\r\n/**\r\n * @module tiny_cursive/append_submissions_table\r\n * @category TinyMCE Editor\r\n * @copyright CTI \r\n * @author kuldeep singh \r\n */\r\n\r\ndefine([\r\n \"jquery\",\r\n \"core/ajax\",\r\n \"core/str\",\r\n \"core/templates\",\r\n \"./replay\",\r\n './analytic_button',\r\n './replay_button',\r\n './analytic_events',\r\n 'core/str'], function(\r\n $,\r\n AJAX,\r\n str,\r\n templates,\r\n Replay,\r\n analyticButton,\r\n replayButton,\r\n AnalyticEvents,\r\n Str\r\n) {\r\n const replayInstances = {};\r\n // eslint-disable-next-line camelcase\r\n window.video_playback = function(mid, filepath) {\r\n\r\n if (filepath !== '') {\r\n const replay = new Replay(\r\n 'content' + mid,\r\n filepath,\r\n 10,\r\n false,\r\n 'player_' + mid\r\n );\r\n replayInstances[mid] = replay;\r\n } else {\r\n templates.render('tiny_cursive/no_submission').then(html => {\r\n $('#content' + mid).html(html);\r\n return true;\r\n }).catch(e => window.console.error(e));\r\n }\r\n return false;\r\n\r\n };\r\n\r\n var usersTable = {\r\n init: function(scoreSetting, showcomment, hasApiKey) {\r\n str\r\n .get_strings([\r\n {key: \"confidence_threshold\", component: \"tiny_cursive\"},\r\n ]).done(function() {\r\n usersTable.appendTable(scoreSetting, hasApiKey);\r\n });\r\n },\r\n appendTable: function(scoreSetting, hasApiKey) {\r\n let subUrl = window.location.href;\r\n let parm = new URL(subUrl);\r\n let hTr = $('thead').find('tr').get()[0];\r\n\r\n Str.get_string('analytics', 'tiny_cursive')\r\n .then(analyticString => {\r\n $(hTr).find('th').eq(3).after('
'\r\n + analyticString + '
' +\r\n '
');\r\n $('tbody').find(\"tr\").get().forEach(function(tr) {\r\n let tdUser = $(tr).find(\"td\").get()[0];\r\n let userid = $(tdUser).find(\"input[type='checkbox']\")?.get()[0]?.value;\r\n let cmid = parm.searchParams.get('id');\r\n // Create the table cell element and append the anchor.\r\n\r\n let args = {id: userid, modulename: \"assign\", cmid: cmid};\r\n let methodname = 'cursive_user_list_submission_stats';\r\n let com = AJAX.call([{methodname, args}]);\r\n try {\r\n com[0].done(function(json) {\r\n var data = JSON.parse(json);\r\n var filepath = '';\r\n if (data.res.filename) {\r\n filepath = data.res.filename;\r\n }\r\n\r\n const tableCell = document.createElement('td');\r\n\r\n if (!hasApiKey) {\r\n $(tableCell).html(replayButton(userid));\r\n } else {\r\n tableCell.appendChild(analyticButton(data.res.effort_ratio, userid));\r\n }\r\n $(tr).find('td').eq(3).after(tableCell);\r\n\r\n // Get Module Name from element.\r\n let element = document.querySelector('.page-header-headings h1');\r\n // Selects the h1 element within the .page-header-headings class\r\n let textContent = element.textContent; // Extracts the text content from the h1 element\r\n\r\n let myEvents = new AnalyticEvents();\r\n var context = {\r\n tabledata: data.res,\r\n formattime: myEvents.formatedTime(data.res),\r\n moduletitle: textContent,\r\n page: scoreSetting,\r\n userid: userid,\r\n apikey: hasApiKey\r\n };\r\n\r\n let authIcon = myEvents.authorshipStatus(data.res.first_file, data.res.score, scoreSetting);\r\n myEvents.createModal(userid, context, '', replayInstances, authIcon);\r\n myEvents.analytics(userid, templates, context, '', replayInstances, authIcon);\r\n myEvents.checkDiff(userid, data.res.file_id, '', replayInstances);\r\n myEvents.replyWriting(userid, filepath, '', replayInstances);\r\n\r\n }).fail(function(error) {\r\n window.console.error('AJAX request failed:', error);\r\n });\r\n } catch (error) {\r\n window.console.error('Error processing data:', error);\r\n }\r\n return com.usercomment;\r\n });\r\n return true;\r\n })\r\n .catch(error => {\r\n window.console.error('Failed to get analytics string:', error);\r\n });\r\n }\r\n };\r\n\r\n return usersTable;\r\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
diff --git a/amd/build/autosaver.min.js.map b/amd/build/autosaver.min.js.map
index e0c9b4de..c5141173 100644
--- a/amd/build/autosaver.min.js.map
+++ b/amd/build/autosaver.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"autosaver.min.js","sources":["../src/autosaver.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/autosaver\n * @category TinyMCE Editor\n * @copyright CTI \n * @author Brain Station 23 \n */\n\nimport {call} from 'core/ajax';\nimport {create} from 'core/modal_factory';\nimport {get_string as getString} from 'core/str';\nimport {save, cancel, hidden} from 'core/modal_events';\nimport $ from 'jquery';\nimport {iconUrl, iconGrayUrl, tooltipCss} from 'tiny_cursive/common';\nimport Autosave from 'tiny_cursive/cursive_autosave';\nimport DocumentView from 'tiny_cursive/document_view';\nimport {call as getUser} from \"core/ajax\";\n\nexport const register = (editor, interval, userId, hasApiKey, MODULES, Rubrics, submission, quizInfo, pasteSetting) => {\n\n var isStudent = !($('#body').hasClass('teacher_admin'));\n var intervention = $('#body').hasClass('intervention');\n var host = M.cfg.wwwroot;\n var userid = userId;\n var courseid = M.cfg.courseId;\n var editorid = editor?.id;\n var cmid = M.cfg.contextInstanceId;\n var ed = \"\";\n var event = \"\";\n var filename = \"\";\n var questionid = 0;\n var quizSubmit = $('#mod_quiz-next-nav');\n let assignSubmit = $('#id_submitbutton');\n var syncInterval = interval ? interval * 1000 : 10000; // Default: Sync Every 10s.\n var lastCaretPos = 1;\n let aiContents = [];\n var isFullScreen = false;\n var user = null;\n let ur = window.location.href;\n let parm = new URL(ur);\n let modulesInfo = getModulesInfo(ur, parm, MODULES);\n var resourceId = modulesInfo.resourceId;\n var modulename = modulesInfo.name;\n var errorAlert = true;\n let PASTE_SETTING = pasteSetting || 'allow';\n let shouldBlockPaste = false;\n let isPasteAllowed = false;\n\n if (ur.includes('pdfannotator')) {\n document.addEventListener('click', e => {\n if (e.target.className === \"dropdown-item comment-edit-a\") {\n let id = e.target.id;\n resourceId = id.replace('editButton', '');\n localStorage.setItem('isEditing', '1');\n }\n if (e.target.id === 'commentSubmit') {\n syncData();\n }\n });\n }\n\n const postOne = async(methodname, args) => {\n try {\n const response = await call([{\n methodname,\n args,\n }])[0];\n if (response) {\n setTimeout(() => {\n Autosave.updateSavingState('saved');\n }, 1000);\n }\n return response;\n } catch (error) {\n Autosave.updateSavingState('offline');\n window.console.error('Error in postOne:', error);\n throw error;\n }\n };\n\n getUser([{\n methodname: 'core_user_get_users_by_field',\n args: {field: 'id', values: [userid]},\n }])[0].done(response => {\n user = response[0];\n }).fail((ex) => {\n window.console.error('Error fetching user data:', ex);\n });\n\n assignSubmit.on('click', async function(e) {\n e.preventDefault();\n if (filename) {\n // eslint-disable-next-line\n syncData().then(() => {\n assignSubmit.off('click').click();\n });\n } else {\n assignSubmit.off('click').click();\n }\n localStorage.removeItem('lastCopyCutContent');\n });\n\n quizSubmit.on('click', async function(e) {\n e.preventDefault();\n if (filename) {\n // eslint-disable-next-line\n syncData().then(() => {\n quizSubmit.off('click').click();\n });\n } else {\n quizSubmit.off('click').click();\n }\n localStorage.removeItem('lastCopyCutContent');\n });\n\n const getModal = () => {\n\n Promise.all([\n getString('tiny_cursive_srcurl', 'tiny_cursive'),\n getString('tiny_cursive_srcurl_des', 'tiny_cursive'),\n getString('tiny_cursive_placeholder', 'tiny_cursive')\n ]).then(function([title, titledes, placeholder]) {\n\n return create({\n type: 'SAVE_CANCEL',\n title: `
${title}
\n ${titledes}
`,\n body: ``,\n removeOnClose: true,\n })\n .done(modal => {\n modal.getRoot().addClass('tiny-cursive-modal');\n modal.show();\n var lastEvent = '';\n\n modal.getRoot().on(save, function() {\n\n var number = document.getElementById(\"inputUrl\").value.trim();\n\n if (number === \"\" || number === null || number === undefined) {\n editor.execCommand('Undo');\n // eslint-disable-next-line\n getString('pastewarning', 'tiny_cursive').then(str => alert(str));\n } else {\n editor.execCommand('Paste');\n }\n\n postOne('cursive_user_comments', {\n modulename: modulename,\n cmid: cmid,\n resourceid: resourceId,\n courseid: courseid,\n usercomment: number,\n timemodified: Date.now(),\n editorid: editorid ? editorid : \"\"\n });\n\n lastEvent = 'save';\n modal.destroy();\n });\n modal.getRoot().on(cancel, function() {\n editor.execCommand('Undo');\n lastEvent = 'cancel';\n });\n\n modal.getRoot().on(hidden, function() {\n if (lastEvent != 'cancel' && lastEvent != 'save') {\n editor.execCommand('Undo');\n }\n });\n return modal;\n });\n }).catch(error => window.console.error(error));\n\n };\n\n const sendKeyEvent = (events, editor) => {\n ed = editor;\n event = events;\n\n filename = `${userid}_${resourceId}_${cmid}_${modulename}_attempt`;\n\n if (modulename === 'quiz') {\n questionid = editorid.split(':')[1].split('_')[0];\n filename = `${userid}_${resourceId}_${cmid}_${questionid}_${modulename}_attempt`;\n }\n\n if (localStorage.getItem(filename)) {\n let data = JSON.parse(localStorage.getItem(filename));\n data.push({\n resourceId: resourceId,\n key: editor.key,\n keyCode: editor.keyCode,\n event: event,\n courseId: courseid,\n unixTimestamp: Date.now(),\n clientId: host,\n personId: userid,\n position: ed.caretPosition,\n rePosition: ed.rePosition,\n pastedContent: editor.pastedContent,\n aiContent: editor.aiContent\n });\n localStorage.setItem(filename, JSON.stringify(data));\n } else {\n let data = [{\n resourceId: resourceId,\n key: editor.key,\n keyCode: editor.keyCode,\n event: event,\n courseId: courseid,\n unixTimestamp: Date.now(),\n clientId: host,\n personId: userid,\n position: ed.caretPosition,\n rePosition: ed.rePosition,\n pastedContent: editor.pastedContent,\n aiContent: editor.aiContent\n }];\n localStorage.setItem(filename, JSON.stringify(data));\n }\n };\n\n editor.on('keyUp', (editor) => {\n customTooltip();\n let position = getCaretPosition(false);\n editor.caretPosition = position.caretPosition;\n editor.rePosition = position.rePosition;\n sendKeyEvent(\"keyUp\", editor);\n });\n editor.on('Paste', async(e) => {\n customTooltip();\n const pastedContent = (e.clipboardData || e.originalEvent.clipboardData).getData('text');\n if (!pastedContent) {\n return;\n }\n // Trim both values for consistent comparison\n const trimmedPastedContent = pastedContent.trim();\n const lastCopyCutContent = localStorage.getItem('lastCopyCutContent');\n const isFromOwnEditor = lastCopyCutContent && trimmedPastedContent === lastCopyCutContent;\n\n if (isStudent && intervention) {\n\n if (PASTE_SETTING === 'block') {\n if (!isFromOwnEditor) {\n e.preventDefault();\n shouldBlockPaste = true;\n isPasteAllowed = false;\n e.stopPropagation();\n e.stopImmediatePropagation();\n getString('paste_blocked', 'tiny_cursive').then(str => {\n return editor.windowManager.alert(str);\n }).catch(error => window.console.error(error));\n setTimeout(() => {\n isPasteAllowed = true;\n shouldBlockPaste = false;\n }, 100);\n return;\n }\n shouldBlockPaste = false;\n isPasteAllowed = true;\n return;\n }\n if (PASTE_SETTING === 'cite_source') {\n if (!isFromOwnEditor) {\n e.preventDefault();\n e.stopPropagation();\n e.stopImmediatePropagation();\n getModal(e);\n }\n isPasteAllowed = true;\n return;\n }\n }\n isPasteAllowed = true;\n });\n editor.on('Redo', async(e) => {\n customTooltip();\n if (isStudent && intervention) {\n getModal(e);\n }\n });\n editor.on('keyDown', (editor) => {\n customTooltip();\n const isPasteAttempt = (editor.key === 'v' || editor.key === 'V') &&\n (editor.ctrlKey || editor.metaKey);\n if (isPasteAttempt && isStudent && intervention && PASTE_SETTING === 'block' && !isPasteAllowed) {\n setTimeout(() => {\n isPasteAllowed = true;\n }, 100);\n return;\n }\n let position = getCaretPosition();\n editor.caretPosition = position.caretPosition;\n editor.rePosition = position.rePosition;\n sendKeyEvent(\"keyDown\", editor);\n });\n editor.on('Cut', () => {\n const selectedContent = editor.selection.getContent({format: 'text'});\n localStorage.setItem('lastCopyCutContent', selectedContent.trim());\n });\n editor.on('Copy', () => {\n const selectedContent = editor.selection.getContent({format: 'text'});\n localStorage.setItem('lastCopyCutContent', selectedContent.trim());\n });\n editor.on('mouseDown', async(editor) => {\n setTimeout(() => {\n constructMouseEvent(editor);\n sendKeyEvent(\"mouseDown\", editor);\n }, 0);\n });\n editor.on('mouseUp', async(editor) => {\n setTimeout(() => {\n constructMouseEvent(editor);\n sendKeyEvent(\"mouseUp\", editor);\n }, 10);\n });\n editor.on('init', () => {\n customTooltip();\n localStorage.removeItem('lastCopyCutContent');\n });\n editor.on('SetContent', () => {\n customTooltip();\n });\n editor.on('FullscreenStateChanged', (e) => {\n let view = new DocumentView(user, Rubrics, submission, modulename, editor, quizInfo);\n isFullScreen = e.state;\n try {\n if (!e.state) {\n view.normalMode();\n } else {\n view.fullPageMode();\n }\n } catch (error) {\n if (errorAlert) {\n errorAlert = false;\n getString('fullmodeerror', 'tiny_cursive').then(str => {\n return editor.windowManager.alert(str);\n }).catch(error => window.console.error(error));\n }\n view.normalMode();\n window.console.error('Error ResizeEditor event:', error);\n }\n });\n\n editor.on('execcommand', function(e) {\n if (e.command === \"mceInsertContent\") {\n const contentObj = e.value;\n\n const isPaste = contentObj && typeof contentObj === 'object' && contentObj.paste === true;\n\n let insertedContent = contentObj.content || contentObj;\n let tempDiv = document.createElement('div');\n tempDiv.innerHTML = insertedContent;\n let text = tempDiv.textContent || tempDiv.innerText || '';\n let pastedText = tempDiv.textContent || tempDiv.innerText || '';\n\n let position = getCaretPosition(true);\n editor.caretPosition = position.caretPosition;\n editor.rePosition = position.rePosition;\n\n if (isPaste) {\n if (shouldBlockPaste) {\n shouldBlockPaste = false;\n e.preventDefault();\n editor.undoManager.undo();\n return;\n }\n const lastCopyCutContent = localStorage.getItem('lastCopyCutContent');\n const isFromOwnEditor = lastCopyCutContent && pastedText.trim() === lastCopyCutContent;\n\n if (isStudent && intervention && PASTE_SETTING === 'block' && !isFromOwnEditor) {\n isPasteAllowed = false;\n editor.undoManager.undo();\n return;\n }\n\n sendKeyEvent(\"Paste\", {\n key: \"v\",\n keyCode: 86,\n caretPosition: editor.caretPosition,\n rePosition: editor.rePosition,\n pastedContent: pastedText,\n srcElement: {baseURI: window.location.href}\n });\n } else {\n aiContents.push(text);\n\n sendKeyEvent(\"aiInsert\", {\n key: \"ai\",\n keyCode: 0,\n caretPosition: editor.caretPosition,\n rePosition: editor.rePosition,\n aiContent: text,\n srcElement: {baseURI: window.location.href}\n });\n }\n }\n });\n\n editor.on('input', function(e) {\n let position = getCaretPosition(true);\n editor.caretPosition = position.caretPosition;\n editor.rePosition = position.rePosition;\n let aiContent = e.data;\n\n if (e.inputType === 'insertReplacementText' || (e.inputType === 'insertText' && aiContent && aiContent.length > 1)) {\n\n aiContents.push(aiContent);\n\n e.key = \"ai\";\n e.keyCode = 0;\n e.caretPosition = position.caretPosition;\n e.rePosition = position.rePosition;\n e.aiContent = aiContent;\n\n sendKeyEvent(\"aiInsert\", e);\n }\n });\n\n\n /**\n * Constructs a mouse event object with caret position and button information\n * @param {Object} editor - The TinyMCE editor instance\n * @function constructMouseEvent\n * @description Sets caret position, reposition, key and keyCode properties on the editor object based on current mouse state\n */\n function constructMouseEvent(editor) {\n let position = getCaretPosition(false);\n editor.caretPosition = position.caretPosition;\n editor.rePosition = position.rePosition;\n editor.key = getMouseButton(editor);\n editor.keyCode = editor.button;\n }\n\n /**\n * Gets the string representation of a mouse button based on its numeric value\n * @param {Object} editor - The editor object containing button information\n * @returns {string} The string representation of the mouse button ('left', 'middle', or 'right')\n */\n function getMouseButton(editor) {\n\n switch (editor.button) {\n case 0:\n return 'left';\n case 1:\n return 'middle';\n case 2:\n return 'right';\n }\n return null;\n }\n\n /**\n * Gets the current caret position in the editor\n * @param {boolean} skip - If true, returns the last known caret position instead of calculating a new one\n * @returns {Object} Object containing:\n * - caretPosition: Sequential position number stored in session\n * - rePosition: Absolute character offset from start of content\n * @throws {Error} Logs warning to console if error occurs during calculation\n */\n function getCaretPosition(skip = false) {\n try {\n if (!editor || !editor.selection) {\n return {caretPosition: 0, rePosition: 0};\n }\n\n const range = editor.selection.getRng();\n const body = editor.getBody();\n\n // Create a range from start of document to current caret\n const preCaretRange = range.cloneRange();\n preCaretRange.selectNodeContents(body);\n preCaretRange.setEnd(range.endContainer, range.endOffset);\n\n const fragment = preCaretRange.cloneContents();\n const tempDiv = document.createElement('div');\n tempDiv.appendChild(fragment);\n let textBeforeCursor = tempDiv.innerText || '';\n\n const endContainer = range.endContainer;\n const endOffset = range.endOffset;\n\n if (endOffset === 0 &&\n endContainer.nodeType === Node.ELEMENT_NODE &&\n editor.dom.isBlock(endContainer) &&\n endContainer.previousSibling) {\n textBeforeCursor += '\\n';\n }\n const blockElements = tempDiv.querySelectorAll('p, div, h1, h2, h3, h4, h5, h6, li');\n let emptyBlockCount = 0;\n blockElements.forEach(block => {\n const text = block.innerText || block.textContent || '';\n if (text.trim() === '' && block.childNodes.length === 1 &&\n block.childNodes[0].nodeName === 'BR') {\n emptyBlockCount++;\n }\n });\n\n // Add newlines for empty blocks (these represent Enter presses that created empty lines)\n if (emptyBlockCount > 0) {\n textBeforeCursor += '\\n'.repeat(emptyBlockCount);\n }\n\n const absolutePosition = textBeforeCursor.length;\n\n if (skip) {\n return {\n caretPosition: lastCaretPos,\n rePosition: absolutePosition\n };\n }\n // Increment sequential caretPosition\n const storageKey = `${userid}_${resourceId}_${cmid}_position`;\n let storedPos = parseInt(sessionStorage.getItem(storageKey), 10);\n if (isNaN(storedPos)) {\n storedPos = 0;\n }\n storedPos++;\n lastCaretPos = storedPos;\n sessionStorage.setItem(storageKey, storedPos);\n\n return {\n caretPosition: storedPos,\n rePosition: absolutePosition\n };\n\n } catch (e) {\n window.console.warn('Error getting caret position:', e);\n return {caretPosition: lastCaretPos || 1, rePosition: 0};\n }\n }\n\n\n /**\n * Synchronizes data from localStorage to server\n * @async\n * @function SyncData\n * @description Retrieves stored keypress data from localStorage and sends it to server\n * @returns {Promise} Returns response from server if data exists and is successfully sent\n * @throws {Error} Logs error to console if data submission fails\n */\n async function syncData() {\n checkIsPdfAnnotator();\n let data = localStorage.getItem(filename);\n\n if (!data || data.length === 0) {\n return;\n } else {\n localStorage.removeItem(filename);\n editor.fire('change');\n let originalText = editor.getContent({format: 'text'});\n if (!originalText) {\n originalText = getRawText(editor);\n }\n try {\n Autosave.updateSavingState('saving');\n // eslint-disable-next-line\n return await postOne('cursive_write_local_to_json', {\n key: ed.key,\n event: event,\n keyCode: ed.keyCode,\n resourceId: resourceId,\n cmid: cmid,\n modulename: modulename,\n editorid: editorid,\n \"json_data\": data,\n originalText: originalText\n });\n } catch (error) {\n window.console.error('Error submitting data:', error);\n }\n }\n }\n\n /**\n * Gets the raw text content from a TinyMCE editor iframe\n * @param {Object} editor - The TinyMCE editor instance\n * @returns {string} The raw text content of the editor body, or empty string if not found\n * @description Attempts to get the raw text content from the editor's iframe body by:\n * 1. Getting the editor ID\n * 2. Finding the associated iframe element\n * 3. Accessing the iframe's document body\n * 4. Returning the text content\n * Returns empty string if any step fails\n */\n function getRawText(editor) {\n let editorId = editor?.id;\n if (editorId) {\n let iframe = document.querySelector(`#${editorId}_ifr`);\n let iframeBody = iframe.contentDocument?.body || iframe.contentWindow?.document?.body;\n return iframeBody?.textContent;\n }\n return \"\";\n }\n\n /**\n * Sets up custom tooltip functionality for the Cursive icon\n * Initializes tooltip text, positions the icon in the menubar,\n * and sets up mouse event handlers for showing/hiding the tooltip\n * @function customTooltip\n */\n function customTooltip() {\n try {\n const tooltipText = getTooltipText();\n const menubarDiv = document.querySelectorAll('div[role=\"menubar\"].tox-menubar');\n let classArray = [];\n\n if (menubarDiv.length) {\n menubarDiv.forEach(function(element, index) {\n index += 1;\n let className = 'cursive-menu-' + index;\n element.classList.add(className);\n classArray.push(className);\n });\n }\n\n const cursiveIcon = document.createElement('img');\n cursiveIcon.src = hasApiKey ? iconUrl : iconGrayUrl;\n\n cursiveIcon.setAttribute('class', 'tiny_cursive_StateButton');\n cursiveIcon.style.display = 'inline-block';\n\n cursiveState(cursiveIcon, menubarDiv, classArray);\n\n for (let index in classArray) {\n const elementId = \"tiny_cursive_StateIcon\" + index;\n const tooltipId = `tiny_cursive_tooltip${index}`;\n\n tooltipText.then((text) => {\n return setTooltip(text, document.querySelector(`#${elementId}`), tooltipId);\n }).catch(error => window.console.error(error));\n\n $(`#${elementId}`).on('mouseenter', function() {\n $(this).css('position', 'relative');\n $(`#${tooltipId}`).css(tooltipCss);\n });\n\n $(`#${elementId}`).on('mouseleave', function() {\n $(`#${tooltipId}`).css('display', 'none');\n });\n }\n } catch (error) {\n window.console.error('Error setting up custom tooltip:', error);\n }\n }\n\n /**\n * Retrieves tooltip text strings from language files\n * @async\n * @function getTooltipText\n * @returns {Promise