diff --git a/amd/build/dialog.min.js b/amd/build/dialog.min.js index 555a6cf..254da35 100644 --- a/amd/build/dialog.min.js +++ b/amd/build/dialog.min.js @@ -1,3 +1,3 @@ -define("block_ai_chat/dialog",["exports","core/modal","block_ai_chat/webservices","core/templates","core/notification","core/modal_events","block_ai_chat/helper","block_ai_chat/ai_manager","core/str","local_ai_manager/infobox","local_ai_manager/userquota","local_ai_manager/config","core/localstorage","./helper","core/config"],(function(_exports,_modal,externalServices,_templates,_notification,_modal_events,helper,manager,_str,_infobox,_userquota,_config,_localstorage,_helper2,_config2){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_modal=_interopRequireDefault(_modal),externalServices=_interopRequireWildcard(externalServices),_templates=_interopRequireDefault(_templates),_modal_events=_interopRequireDefault(_modal_events),helper=_interopRequireWildcard(helper),manager=_interopRequireWildcard(manager),_localstorage=_interopRequireDefault(_localstorage),_config2=_interopRequireDefault(_config2);const VIEW_CHATWINDOW="block_ai_chat_chatwindow",VIEW_OPENFULL="block_ai_chat_openfull",VIEW_DOCKRIGHT="block_ai_chat_dockright";let strHistory,strNewDialog,strToday,strYesterday,badge,viewmode,modal={},modalopen=!1,conversation={id:0,messages:[]},allConversations=[],userid=0,contextid=0,firstLoad=!0,aiAtWork=!1,maxHistory=5,maxHistoryWarnings=new Set,tenantConfig={},chatConfig={};class DialogModal extends _modal.default{configure(modalConfig){modalConfig.show=!1,modalConfig.removeOnClose=!1,modalConfig.isVerticallyCentered=!1,super.configure(modalConfig),modalConfig.titletest&&this.setTitletest(modalConfig.titletest)}setTitletest(value){this.titletest=value}hide(){super.hide(),modalopen=!1;document.querySelector("body").classList.remove("block_ai_chat_open")}}_defineProperty(DialogModal,"TYPE","block_ai_chat/dialog_modal"),_defineProperty(DialogModal,"TEMPLATE","block_ai_chat/dialog_modal");_exports.init=async params=>{userid=params.userid,contextid=params.contextid,strNewDialog=params.new,strHistory=params.history,badge=params.badge,badge=!1;const aiConfig=await(0,_config.getAiConfig)();tenantConfig=aiConfig,chatConfig=aiConfig.purposes.find((p=>"chat"===p.purpose)),modal=await DialogModal.create({templateContext:{title:strNewDialog,badge:false}}),modal.getRoot().on("modal:shown",(function(e){e.target.classList.add("ai_chat_modal")})),modal.getRoot().on(_modal_events.default.outsideClick,(event=>{checkOutsideClick(event)})),setView(),document.getElementById("ai_chat_button").addEventListener("mousedown",(async()=>{!async function(){if(modalopen)return void modal.hide();await modal.show(),modalopen=!0;document.querySelector("body").classList.add("block_ai_chat_open");const textarea=document.getElementById("block_ai_chat-input-id");addTextareaListener(textarea);if(document.getElementById("block_ai_chat-submit-id").addEventListener("click",(event=>{clickSubmitButton(event)})),firstLoad){await getConversations(),showConversation();let conversationcontextLimit=await externalServices.getConversationcontextLimit(contextid);maxHistory=conversationcontextLimit.limit;document.getElementById("block_ai_chat_new_dialog").addEventListener("click",(()=>{newDialog()}));document.getElementById("block_ai_chat_delete_dialog").addEventListener("click",(()=>{deleteCurrentDialog()}));document.getElementById("block_ai_chat_show_history").addEventListener("click",(()=>{showHistory()}));document.getElementById(VIEW_CHATWINDOW).addEventListener("click",(()=>{setView(VIEW_CHATWINDOW)}));document.getElementById(VIEW_OPENFULL).addEventListener("click",(()=>{setView(VIEW_OPENFULL)}));document.getElementById(VIEW_DOCKRIGHT).addEventListener("click",(()=>{setView(VIEW_DOCKRIGHT)})),await(0,_userquota.renderUserQuota)("#block_ai_chat_userquota",["chat"]),await(0,_infobox.renderInfoBox)("block_ai_chat",userid,'.ai_chat_modal_body [data-content="local_ai_manager_infobox"]',["chat"]);const message=await userAllowed();if(""!==message){const notice=await(0,_str.getString)("notice","block_ai_chat");await(0,_notification.alert)(notice,message)}firstLoad=!1}helper.focustextarea()}()})),strToday=await(0,_str.getString)("today","core"),strYesterday=await(0,_str.getString)("yesterday","block_ai_chat");window.matchMedia("(max-width: 576px)").addEventListener("change",handleScreenWidthChange),window.innerWidth<=576&&setView(VIEW_OPENFULL)};const getConversations=async()=>{try{allConversations=await externalServices.getAllConversations(userid,contextid)}catch(error){(0,_notification.exception)(error)}},showConversation=function(){let id=arguments.length>0&&void 0!==arguments[0]?arguments[0]:0;aiAtWork||(0!==id?conversation=allConversations.find((x=>x.id===id)):void 0!==allConversations[0]?conversation=allConversations.at(allConversations.length-1):0===allConversations.length&&newDialog(!0),clearMessages(),setModalHeader(),showMessages())};document.showConversation=showConversation;const enterQuestion=async question=>{if(""==question)return void(aiAtWork=!1);const message=await userAllowed();if(""!==message){const notice=await(0,_str.getString)("noticenewquestion","block_ai_chat");return await(0,_notification.alert)(notice,message),void(aiAtWork=!1)}if(showMessage(question,"self",!1),0===conversation.messages.length){const currentUserLanguage=_config2.default.language.substring(0,2),LangNames=new Intl.DisplayNames("en",{type:"language"});conversation.messages.push({message:"Answer in "+LangNames.of(currentUserLanguage),sender:"system"})}const convHistory=await checkMessageHistoryLengthLimit(conversation.messages),options={component:"block_ai_chat",contextid:contextid,conversationcontext:convHistory};if(0===conversation.id){try{let idresult=await externalServices.getNewConversationId(contextid);conversation.id=idresult.id,conversation.timecreated=Math.floor(Date.now()/1e3),setModalHeader((0,_helper2.escapeHTML)(question))}catch(error){(0,_notification.exception)(error)}options.forcenewitemid=!0}options.itemid=conversation.id;let requestresult=await manager.askLocalAiManager("chat",question,options);200!=requestresult.code&&(requestresult=await errorHandling(requestresult,question,options));let copy=document.querySelector(".ai_chat_modal .awaitanswer .copy");copy.addEventListener("mousedown",(()=>{helper.copyToClipboard(copy)})),showReply(requestresult.result),aiAtWork=!1,saveConversationLocally(question,requestresult.result);document.getElementById("block_ai_chat_userquota").innerHTML="",(0,_userquota.renderUserQuota)("#block_ai_chat_userquota",["chat"])},showReply=async text=>{let fields=document.querySelectorAll(".ai_chat_modal .awaitanswer .text");const field=fields[fields.length-1];field.innerHTML=text,field.classList.remove("small");let awaitdivs=document.querySelectorAll(".ai_chat_modal .awaitanswer");awaitdivs[awaitdivs.length-1].classList.remove("awaitanswer")},showMessages=()=>{conversation.messages.forEach((val=>{showMessage(val.message,val.sender)}))},showMessage=async function(text){let sender=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",answer=!(arguments.length>2&&void 0!==arguments[2])||arguments[2];if("system"===sender)return;"ai"===sender&&(sender=""),answer||(text=(0,_helper2.escapeHTML)(text));const templateData={sender:sender,content:text,answer:answer},{html:html,js:js}=await _templates.default.renderForPromise("block_ai_chat/message",templateData);_templates.default.appendNodeContents(".block_ai_chat-output",html,js),""===sender&&helper.attachCopyListenerLast(),helper.scrollToBottom()},newDialog=async function(){let deleted=arguments.length>0&&void 0!==arguments[0]&&arguments[0];aiAtWork||(void 0!==allConversations.find((x=>x.id===conversation.id))||deleted||allConversations.push(conversation),conversation={id:0,messages:[]},clearMessages(),setModalHeader(strNewDialog),helper.focustextarea())},deleteCurrentDialog=()=>{(0,_notification.deleteCancelPromise)((0,_str.getString)("delete","block_ai_chat"),(0,_str.getString)("deletewarning","block_ai_chat")).then((async()=>{if(0!==conversation.id)try{await externalServices.deleteConversation(contextid,userid,conversation.id)&&(removeFromHistory(),showConversation())}catch(error){(0,_notification.exception)(error)}})).catch((()=>{}))},showHistory=async()=>{void 0===allConversations.find((x=>x.id===conversation.id))&&allConversations.push(conversation);let title=''+strHistory+"";clearMessages(!0),setModalHeader(title);document.getElementById("block_ai_chat_backlink").addEventListener("click",(()=>{0!==conversation.id?showConversation(conversation.id):newDialog(),clearMessages(),setModalHeader()})),document.querySelector(".ai_chat_modal").classList.add("onhistorypage");let groupedByDate={};allConversations.forEach((convo=>{if(void 0!==convo.messages[1]){let title=convo.messages[1].message;const now=new Date,date=new Date(1e3*convo.timecreated),today=new Date(now.getFullYear(),now.getMonth(),now.getDate()),yesterday=new Date(now.getFullYear(),now.getMonth(),now.getDate()-1),twoWeeksAgo=new Date(now);twoWeeksAgo.setDate(now.getDate()-14);const options={weekday:"long",day:"2-digit",month:"2-digit"},monthOptions={month:"long",year:"2-digit"};let dateString="";dateString=date>=today?strToday:date>=yesterday?strYesterday:date>=twoWeeksAgo?date.toLocaleDateString(void 0,options):date.toLocaleDateString(void 0,monthOptions);const hours=date.getHours(),minutes=date.getMinutes().toString().padStart(2,"0");let convItem={title:title,conversationid:convo.id,time:hours+":"+minutes};groupedByDate[dateString]||(groupedByDate[dateString]=[]),groupedByDate[dateString].push(convItem)}}));const templateData={dates:{groups:Object.keys(groupedByDate).map((key=>({key:key,objects:groupedByDate[key]})))}.groups},{html:html,js:js}=await _templates.default.renderForPromise("block_ai_chat/history",templateData);_templates.default.appendNodeContents(".ai_chat_modal .block_ai_chat-output",html,js);document.getElementById("ai_chat_history_new_dialog").addEventListener("mousedown",(()=>{newDialog()}))},removeFromHistory=()=>{0!==conversation.id&&void 0!==allConversations.find((x=>x.id===conversation.id))&&(allConversations=allConversations.filter((obj=>obj.id!==conversation.id)))},saveConversationLocally=(question,reply)=>{let message={message:question,sender:"user"};conversation.messages.push(message),message={message:reply,sender:"ai"},conversation.messages.push(message)},clearMessages=function(){let hideinput=arguments.length>0&&void 0!==arguments[0]&&arguments[0];const output=document.querySelector(".block_ai_chat-output");output.innerHTML="";let input=document.querySelector(".block_ai_chat-input");input.style.display=hideinput?"none":"flex"},setModalHeader=function(){let setTitle=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"",modalheader=document.querySelector(".ai_chat_modal .modal-title div"),title="";null!==modalheader&&(conversation.messages.length>0||setTitle.length)&&(title=setTitle.length?setTitle:conversation.messages[1].message,modalheader.innerHTML=title);let modal=document.querySelector(".ai_chat_modal");modal.classList.remove("onhistorypage")},addTextareaListener=textarea=>{textarea.addEventListener("keydown",(event=>{textareaOnKeydown(event),textarea.style.height="auto";const computedStyles=window.getComputedStyle(textarea),lineHeight=parseFloat(computedStyles.lineHeight),paddingTop=parseFloat(computedStyles.paddingTop),paddingBottom=parseFloat(computedStyles.paddingBottom),borderTop=parseFloat(computedStyles.borderTopWidth),borderBottom=parseFloat(computedStyles.borderBottomWidth),maxHeight=4*lineHeight+paddingTop+paddingBottom+borderTop+borderBottom,newHeight=Math.min(textarea.scrollHeight+borderTop+borderBottom,maxHeight);textarea.style.height=newHeight+"px"}))},textareaOnKeydown=event=>{"Enter"!==event.key||aiAtWork||event.shiftKey||(aiAtWork=!0,enterQuestion(event.target.value),event.preventDefault(),event.target.value="")},clickSubmitButton=()=>{if(!aiAtWork){aiAtWork=!0;const textarea=document.getElementById("block_ai_chat-input-id");enterQuestion(textarea.value),textarea.value=""}},errorHandling=async(requestresult,question,options)=>{if(409==requestresult.code)for(;409==requestresult.code;){try{let idresult=await externalServices.getNewConversationId(contextid);conversation.id=idresult.id,options.itemid=conversation.id}catch(error){(0,_notification.exception)(error)}return requestresult=await manager.askLocalAiManager("chat",question,options)}const errorString=await(0,_str.getString)("errorwithcode","block_ai_chat",requestresult.code),result=JSON.parse(requestresult.result);await(0,_notification.alert)(errorString,result.message);const answerdivs=document.querySelectorAll(".awaitanswer");return answerdivs[answerdivs.length-1].closest(".message").classList.add("text-danger"),requestresult.result=await(0,_str.getString)("error","block_ai_chat"),requestresult},checkMessageHistoryLengthLimit=async messages=>{if(messages.length>maxHistory){let shortenedMessages=[messages[0],...messages.slice(-maxHistory)];if(!maxHistoryWarnings.has(conversation.id)){const maxHistoryString=await(0,_str.getString)("maxhistory","block_ai_chat",maxHistory),warningErrorString=await(0,_str.getString)("maxhistoryreached","block_ai_chat",maxHistory);await(0,_notification.alert)(maxHistoryString,warningErrorString),maxHistoryWarnings.add(conversation.id)}return shortenedMessages}return messages},checkOutsideClick=event=>{viewmode!=VIEW_OPENFULL&&event.preventDefault()},setView=async function(){let mode=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"";const key=await(0,_helper2.hash)("chatmode"+userid);let savedmode=_localstorage.default.get(key);""==mode&&(mode=savedmode||VIEW_CHATWINDOW),_localstorage.default.set(key,mode),viewmode=mode;const body=document.querySelector("body");body.classList.remove(VIEW_CHATWINDOW,VIEW_OPENFULL,VIEW_DOCKRIGHT),body.classList.add(mode)},userAllowed=async()=>{let message;if(!1===tenantConfig.tenantenabled)return message=await(0,_str.getString)("error_http403disabled","local_ai_manager"),message;if(!1===tenantConfig.userconfirmed){message=await(0,_str.getString)("error_http403notconfirmed","local_ai_manager"),message+=". ";const link=window.location.origin+"/local/ai_manager/confirm_ai_usage.php";return message+=await(0,_str.getString)("confirm_ai_usage","block_ai_chat",link),message}return!0===tenantConfig.userlocked?(message=await(0,_str.getString)("error_http403blocked","local_ai_manager"),message):!1===chatConfig.isconfigured?(message=await(0,_str.getString)("error_purposenotconfigured","local_ai_manager"),message):!0===chatConfig.lockedforrole?(message=await(0,_str.getString)("error_http403blocked","local_ai_manager"),message):!0===chatConfig.limitreached?(message=await(0,_str.getString)("error_limitreached","local_ai_manager"),message):""},handleScreenWidthChange=e=>{const body=document.querySelector("body");e.matches?(body.classList.remove(VIEW_CHATWINDOW,VIEW_OPENFULL,VIEW_DOCKRIGHT),body.classList.add(VIEW_OPENFULL)):(body.classList.remove(VIEW_CHATWINDOW,VIEW_OPENFULL,VIEW_DOCKRIGHT),body.classList.add(viewmode))}})); +define("block_ai_chat/dialog",["exports","core/modal","block_ai_chat/webservices","core/templates","core/notification","core/modal_events","block_ai_chat/helper","block_ai_chat/ai_manager","core/str","local_ai_manager/infobox","local_ai_manager/userquota","local_ai_manager/config","core/localstorage","./helper","core/config"],(function(_exports,_modal,externalServices,_templates,_notification,_modal_events,helper,manager,_str,_infobox,_userquota,_config,_localstorage,_helper2,_config2){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_modal=_interopRequireDefault(_modal),externalServices=_interopRequireWildcard(externalServices),_templates=_interopRequireDefault(_templates),_modal_events=_interopRequireDefault(_modal_events),helper=_interopRequireWildcard(helper),manager=_interopRequireWildcard(manager),_localstorage=_interopRequireDefault(_localstorage),_config2=_interopRequireDefault(_config2);const VIEW_CHATWINDOW="block_ai_chat_chatwindow",VIEW_OPENFULL="block_ai_chat_openfull",VIEW_DOCKRIGHT="block_ai_chat_dockright";let strHistory,strNewDialog,strToday,strYesterday,badge,viewmode,modal={},modalopen=!1,conversation={id:0,messages:[]},allConversations=[],userid=0,contextid=0,firstLoad=!0,aiAtWork=!1,maxHistory=5,maxHistoryWarnings=new Set,tenantConfig={},chatConfig={};class DialogModal extends _modal.default{configure(modalConfig){modalConfig.show=!1,modalConfig.removeOnClose=!1,modalConfig.isVerticallyCentered=!1,super.configure(modalConfig),modalConfig.titletest&&this.setTitletest(modalConfig.titletest)}setTitletest(value){this.titletest=value}hide(){super.hide(),modalopen=!1;document.querySelector("body").classList.remove("block_ai_chat_open")}}_defineProperty(DialogModal,"TYPE","block_ai_chat/dialog_modal"),_defineProperty(DialogModal,"TEMPLATE","block_ai_chat/dialog_modal");_exports.init=async params=>{userid=params.userid,contextid=params.contextid,strNewDialog=params.new,strHistory=params.history,badge=params.badge,badge=!1;const aiConfig=await(0,_config.getAiConfig)();tenantConfig=aiConfig,chatConfig=aiConfig.purposes.find((p=>"chat"===p.purpose)),modal=await DialogModal.create({templateContext:{title:strNewDialog,badge:false}}),modal.getRoot().on("modal:shown",(function(e){e.target.classList.add("block_ai_chat_modal")})),modal.getRoot().on(_modal_events.default.outsideClick,(event=>{checkOutsideClick(event)})),setView(),document.getElementById("ai_chat_button").addEventListener("mousedown",(async()=>{!async function(){if(modalopen)return void modal.hide();await modal.show(),modalopen=!0;document.querySelector("body").classList.add("block_ai_chat_open");const textarea=document.getElementById("block_ai_chat-input-id");addTextareaListener(textarea);if(document.getElementById("block_ai_chat-submit-id").addEventListener("click",(event=>{clickSubmitButton(event)})),firstLoad){await getConversations(),showConversation();let conversationcontextLimit=await externalServices.getConversationcontextLimit(contextid);maxHistory=conversationcontextLimit.limit;document.getElementById("block_ai_chat_new_dialog").addEventListener("click",(()=>{newDialog()}));document.getElementById("block_ai_chat_delete_dialog").addEventListener("click",(()=>{deleteCurrentDialog()}));document.getElementById("block_ai_chat_show_history").addEventListener("click",(()=>{showHistory()}));document.getElementById(VIEW_CHATWINDOW).addEventListener("click",(()=>{setView(VIEW_CHATWINDOW)}));document.getElementById(VIEW_OPENFULL).addEventListener("click",(()=>{setView(VIEW_OPENFULL)}));document.getElementById(VIEW_DOCKRIGHT).addEventListener("click",(()=>{setView(VIEW_DOCKRIGHT)})),await(0,_userquota.renderUserQuota)("#block_ai_chat_userquota",["chat"]),await(0,_infobox.renderInfoBox)("block_ai_chat",userid,'.block_ai_chat_modal_body [data-content="local_ai_manager_infobox"]',["chat"]);const message=await userAllowed();if(""!==message){const notice=await(0,_str.getString)("notice","block_ai_chat");await(0,_notification.alert)(notice,message)}firstLoad=!1}helper.focustextarea()}()})),strToday=await(0,_str.getString)("today","core"),strYesterday=await(0,_str.getString)("yesterday","block_ai_chat");window.matchMedia("(max-width: 576px)").addEventListener("change",handleScreenWidthChange),window.innerWidth<=576&&setView(VIEW_OPENFULL)};const getConversations=async()=>{try{allConversations=await externalServices.getAllConversations(userid,contextid)}catch(error){(0,_notification.exception)(error)}},showConversation=function(){let id=arguments.length>0&&void 0!==arguments[0]?arguments[0]:0;aiAtWork||(0!==id?conversation=allConversations.find((x=>x.id===id)):void 0!==allConversations[0]?conversation=allConversations.at(allConversations.length-1):0===allConversations.length&&newDialog(!0),clearMessages(),setModalHeader(),showMessages())};document.showConversation=showConversation;const enterQuestion=async question=>{if(""==question)return void(aiAtWork=!1);const message=await userAllowed();if(""!==message){const notice=await(0,_str.getString)("noticenewquestion","block_ai_chat");return await(0,_notification.alert)(notice,message),void(aiAtWork=!1)}if(showMessage(question,"self",!1),0===conversation.messages.length){const currentUserLanguage=_config2.default.language.substring(0,2),LangNames=new Intl.DisplayNames("en",{type:"language"});conversation.messages.push({message:"Answer in "+LangNames.of(currentUserLanguage),sender:"system"})}const convHistory=await checkMessageHistoryLengthLimit(conversation.messages),options={component:"block_ai_chat",contextid:contextid,conversationcontext:convHistory};if(0===conversation.id){try{let idresult=await externalServices.getNewConversationId(contextid);conversation.id=idresult.id,conversation.timecreated=Math.floor(Date.now()/1e3),setModalHeader((0,_helper2.escapeHTML)(question))}catch(error){(0,_notification.exception)(error)}options.forcenewitemid=!0}options.itemid=conversation.id;let requestresult=await manager.askLocalAiManager("chat",question,options);200!=requestresult.code&&(requestresult=await errorHandling(requestresult,question,options));let copy=document.querySelector(".block_ai_chat_modal .awaitanswer .copy");copy.addEventListener("mousedown",(()=>{helper.copyToClipboard(copy)})),showReply(requestresult.result),aiAtWork=!1,saveConversationLocally(question,requestresult.result);document.getElementById("block_ai_chat_userquota").innerHTML="",(0,_userquota.renderUserQuota)("#block_ai_chat_userquota",["chat"])},showReply=async text=>{let fields=document.querySelectorAll(".block_ai_chat_modal .awaitanswer .text");const field=fields[fields.length-1];field.innerHTML=text,field.classList.remove("small");let awaitdivs=document.querySelectorAll(".block_ai_chat_modal .awaitanswer");awaitdivs[awaitdivs.length-1].classList.remove("awaitanswer")},showMessages=()=>{conversation.messages.forEach((val=>{showMessage(val.message,val.sender)}))},showMessage=async function(text){let sender=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",answer=!(arguments.length>2&&void 0!==arguments[2])||arguments[2];if("system"===sender)return;"ai"===sender&&(sender=""),answer||(text=(0,_helper2.escapeHTML)(text));const templateData={sender:sender,content:text,answer:answer},{html:html,js:js}=await _templates.default.renderForPromise("block_ai_chat/message",templateData);_templates.default.appendNodeContents(".block_ai_chat-output",html,js),""===sender&&helper.attachCopyListenerLast(),helper.scrollToBottom()},newDialog=async function(){let deleted=arguments.length>0&&void 0!==arguments[0]&&arguments[0];aiAtWork||(void 0!==allConversations.find((x=>x.id===conversation.id))||deleted||allConversations.push(conversation),conversation={id:0,messages:[]},clearMessages(),setModalHeader(strNewDialog),helper.focustextarea())},deleteCurrentDialog=()=>{(0,_notification.deleteCancelPromise)((0,_str.getString)("delete","block_ai_chat"),(0,_str.getString)("deletewarning","block_ai_chat")).then((async()=>{if(0!==conversation.id)try{await externalServices.deleteConversation(contextid,userid,conversation.id)&&(removeFromHistory(),showConversation())}catch(error){(0,_notification.exception)(error)}})).catch((()=>{}))},showHistory=async()=>{void 0===allConversations.find((x=>x.id===conversation.id))&&allConversations.push(conversation);let title=''+strHistory+"";clearMessages(!0),setModalHeader(title);document.getElementById("block_ai_chat_backlink").addEventListener("click",(()=>{0!==conversation.id?showConversation(conversation.id):newDialog(),clearMessages(),setModalHeader()})),document.querySelector(".block_ai_chat_modal").classList.add("onhistorypage");let groupedByDate={};allConversations.forEach((convo=>{if(void 0!==convo.messages[1]){let title=convo.messages[1].message;const now=new Date,date=new Date(1e3*convo.timecreated),today=new Date(now.getFullYear(),now.getMonth(),now.getDate()),yesterday=new Date(now.getFullYear(),now.getMonth(),now.getDate()-1),twoWeeksAgo=new Date(now);twoWeeksAgo.setDate(now.getDate()-14);const options={weekday:"long",day:"2-digit",month:"2-digit"},monthOptions={month:"long",year:"2-digit"};let dateString="";dateString=date>=today?strToday:date>=yesterday?strYesterday:date>=twoWeeksAgo?date.toLocaleDateString(void 0,options):date.toLocaleDateString(void 0,monthOptions);const hours=date.getHours(),minutes=date.getMinutes().toString().padStart(2,"0");let convItem={title:title,conversationid:convo.id,time:hours+":"+minutes};groupedByDate[dateString]||(groupedByDate[dateString]=[]),groupedByDate[dateString].push(convItem)}}));const templateData={dates:{groups:Object.keys(groupedByDate).map((key=>({key:key,objects:groupedByDate[key]})))}.groups},{html:html,js:js}=await _templates.default.renderForPromise("block_ai_chat/history",templateData);_templates.default.appendNodeContents(".block_ai_chat_modal .block_ai_chat-output",html,js);document.getElementById("ai_chat_history_new_dialog").addEventListener("mousedown",(()=>{newDialog()}))},removeFromHistory=()=>{0!==conversation.id&&void 0!==allConversations.find((x=>x.id===conversation.id))&&(allConversations=allConversations.filter((obj=>obj.id!==conversation.id)))},saveConversationLocally=(question,reply)=>{let message={message:question,sender:"user"};conversation.messages.push(message),message={message:reply,sender:"ai"},conversation.messages.push(message)},clearMessages=function(){let hideinput=arguments.length>0&&void 0!==arguments[0]&&arguments[0];const output=document.querySelector(".block_ai_chat-output");output.innerHTML="";let input=document.querySelector(".block_ai_chat-input");input.style.display=hideinput?"none":"flex"},setModalHeader=function(){let setTitle=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"",modalheader=document.querySelector(".block_ai_chat_modal .modal-title div"),title="";null!==modalheader&&(conversation.messages.length>0||setTitle.length)&&(title=setTitle.length?setTitle:conversation.messages[1].message,modalheader.innerHTML=title);let modal=document.querySelector(".block_ai_chat_modal");modal.classList.remove("onhistorypage")},addTextareaListener=textarea=>{textarea.addEventListener("keydown",(event=>{textareaOnKeydown(event),textarea.style.height="auto";const computedStyles=window.getComputedStyle(textarea),lineHeight=parseFloat(computedStyles.lineHeight),paddingTop=parseFloat(computedStyles.paddingTop),paddingBottom=parseFloat(computedStyles.paddingBottom),borderTop=parseFloat(computedStyles.borderTopWidth),borderBottom=parseFloat(computedStyles.borderBottomWidth),maxHeight=4*lineHeight+paddingTop+paddingBottom+borderTop+borderBottom,newHeight=Math.min(textarea.scrollHeight+borderTop+borderBottom,maxHeight);textarea.style.height=newHeight+"px"}))},textareaOnKeydown=event=>{"Enter"!==event.key||aiAtWork||event.shiftKey||(aiAtWork=!0,enterQuestion(event.target.value),event.preventDefault(),event.target.value="")},clickSubmitButton=()=>{if(!aiAtWork){aiAtWork=!0;const textarea=document.getElementById("block_ai_chat-input-id");enterQuestion(textarea.value),textarea.value=""}},errorHandling=async(requestresult,question,options)=>{if(409==requestresult.code)for(;409==requestresult.code;){try{let idresult=await externalServices.getNewConversationId(contextid);conversation.id=idresult.id,options.itemid=conversation.id}catch(error){(0,_notification.exception)(error)}return requestresult=await manager.askLocalAiManager("chat",question,options)}const errorString=await(0,_str.getString)("errorwithcode","block_ai_chat",requestresult.code),result=JSON.parse(requestresult.result);await(0,_notification.alert)(errorString,result.message);const answerdivs=document.querySelectorAll(".awaitanswer");return answerdivs[answerdivs.length-1].closest(".message").classList.add("text-danger"),requestresult.result=await(0,_str.getString)("error","block_ai_chat"),requestresult},checkMessageHistoryLengthLimit=async messages=>{if(messages.length>maxHistory){let shortenedMessages=[messages[0],...messages.slice(-maxHistory)];if(!maxHistoryWarnings.has(conversation.id)){const maxHistoryString=await(0,_str.getString)("maxhistory","block_ai_chat",maxHistory),warningErrorString=await(0,_str.getString)("maxhistoryreached","block_ai_chat",maxHistory);await(0,_notification.alert)(maxHistoryString,warningErrorString),maxHistoryWarnings.add(conversation.id)}return shortenedMessages}return messages},checkOutsideClick=event=>{viewmode!=VIEW_OPENFULL&&event.preventDefault()},setView=async function(){let mode=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"";const key=await(0,_helper2.hash)("chatmode"+userid);let savedmode=_localstorage.default.get(key);""==mode&&(mode=savedmode||VIEW_CHATWINDOW),_localstorage.default.set(key,mode),viewmode=mode;const body=document.querySelector("body");body.classList.remove(VIEW_CHATWINDOW,VIEW_OPENFULL,VIEW_DOCKRIGHT),body.classList.add(mode)},userAllowed=async()=>{let message;if(!1===tenantConfig.tenantenabled)return message=await(0,_str.getString)("error_http403disabled","local_ai_manager"),message;if(!1===tenantConfig.userconfirmed){message=await(0,_str.getString)("error_http403notconfirmed","local_ai_manager"),message+=". ";const link=window.location.origin+"/local/ai_manager/confirm_ai_usage.php";return message+=await(0,_str.getString)("confirm_ai_usage","block_ai_chat",link),message}return!0===tenantConfig.userlocked?(message=await(0,_str.getString)("error_http403blocked","local_ai_manager"),message):!1===chatConfig.isconfigured?(message=await(0,_str.getString)("error_purposenotconfigured","local_ai_manager"),message):!0===chatConfig.lockedforrole?(message=await(0,_str.getString)("error_http403blocked","local_ai_manager"),message):!0===chatConfig.limitreached?(message=await(0,_str.getString)("error_limitreached","local_ai_manager"),message):""},handleScreenWidthChange=e=>{const body=document.querySelector("body");e.matches?(body.classList.remove(VIEW_CHATWINDOW,VIEW_OPENFULL,VIEW_DOCKRIGHT),body.classList.add(VIEW_OPENFULL)):(body.classList.remove(VIEW_CHATWINDOW,VIEW_OPENFULL,VIEW_DOCKRIGHT),body.classList.add(viewmode))}})); //# sourceMappingURL=dialog.min.js.map \ No newline at end of file diff --git a/amd/build/dialog.min.js.map b/amd/build/dialog.min.js.map index f03a690..24a061b 100644 --- a/amd/build/dialog.min.js.map +++ b/amd/build/dialog.min.js.map @@ -1 +1 @@ -{"version":3,"file":"dialog.min.js","sources":["../src/dialog.js"],"sourcesContent":["import Modal from 'core/modal';\nimport * as externalServices from 'block_ai_chat/webservices';\nimport Templates from 'core/templates';\nimport {alert as displayAlert, exception as displayException, deleteCancelPromise} from 'core/notification';\nimport ModalEvents from 'core/modal_events';\nimport * as helper from 'block_ai_chat/helper';\nimport * as manager from 'block_ai_chat/ai_manager';\nimport {getString} from 'core/str';\nimport {renderInfoBox} from 'local_ai_manager/infobox';\nimport {renderUserQuota} from 'local_ai_manager/userquota';\nimport {getAiConfig} from 'local_ai_manager/config';\nimport LocalStorage from 'core/localstorage';\nimport {escapeHTML, hash} from './helper';\nimport Config from 'core/config';\n\n// Declare variables.\nconst VIEW_CHATWINDOW = 'block_ai_chat_chatwindow';\nconst VIEW_OPENFULL = 'block_ai_chat_openfull';\nconst VIEW_DOCKRIGHT = 'block_ai_chat_dockright';\nconst MODAL_OPEN = 'block_ai_chat_open';\n\n// Modal.\nlet modal = {};\nlet strHistory;\nlet strNewDialog;\nlet strToday;\nlet strYesterday;\nlet badge;\nlet viewmode;\nlet modalopen = false;\n\n// Current conversation.\nlet conversation = {\n id: 0,\n messages: [],\n};\n// All conversations.\nlet allConversations = [];\n// Userid.\nlet userid = 0;\n// Course context id.\nlet contextid = 0;\n// First load.\nlet firstLoad = true;\n// AI in process of answering.\nlet aiAtWork = false;\n// Maximum history included in query.\nlet maxHistory = 5;\n// Remember warnings for maximum history in this session.\nlet maxHistoryWarnings = new Set();\n// Tenantconfig.\nlet tenantConfig = {};\nlet chatConfig = {};\n\nclass DialogModal extends Modal {\n static TYPE = \"block_ai_chat/dialog_modal\";\n static TEMPLATE = \"block_ai_chat/dialog_modal\";\n\n configure(modalConfig) {\n // Show this modal on instantiation.\n modalConfig.show = false;\n\n // Remove from the DOM on close.\n modalConfig.removeOnClose = false;\n\n modalConfig.isVerticallyCentered = false;\n\n super.configure(modalConfig);\n\n // Accept our own custom arguments too.\n if (modalConfig.titletest) {\n this.setTitletest(modalConfig.titletest);\n }\n }\n\n setTitletest(value) {\n this.titletest = value;\n }\n\n hide() {\n super.hide();\n // Keep track of state, to restrict changes to block_ai_chat modal.\n modalopen = false;\n const body = document.querySelector('body');\n body.classList.remove(MODAL_OPEN);\n }\n}\n\nexport const init = async(params) => {\n // Read params.\n userid = params.userid;\n contextid = params.contextid;\n strNewDialog = params.new;\n strHistory = params.history;\n badge = params.badge;\n // Disable bdage.\n badge = false;\n\n // Get configuration.\n const aiConfig = await getAiConfig();\n tenantConfig = aiConfig;\n chatConfig = aiConfig.purposes.find(p => p.purpose === \"chat\");\n\n // Build modal.\n modal = await DialogModal.create({\n templateContext: {\n title: strNewDialog,\n badge: badge,\n },\n });\n\n // Add class for styling when modal is displayed.\n modal.getRoot().on('modal:shown', function(e) {\n e.target.classList.add(\"ai_chat_modal\");\n });\n\n // Conditionally prevent outside click event.\n modal.getRoot().on(ModalEvents.outsideClick, event => {\n checkOutsideClick(event);\n });\n\n // Check and set viewmode.\n setView();\n\n // Attach listener to the ai button to call modal.\n let button = document.getElementById('ai_chat_button');\n button.addEventListener('mousedown', async() => {\n showModal(params);\n });\n\n // Get strings.\n strToday = await getString('today', 'core');\n strYesterday = await getString('yesterday', 'block_ai_chat');\n\n // Create a MediaQueryList object to check for small screens.\n const mediaQuery = window.matchMedia(\"(max-width: 576px)\");\n\n // Attach the event listener to handle changes.\n mediaQuery.addEventListener('change', handleScreenWidthChange);\n\n // Initial check for screenwidth.\n if (window.innerWidth <= 576) {\n setView(VIEW_OPENFULL);\n }\n};\n\n/**\n * Show ai_chat modal.\n */\nasync function showModal() {\n // Switch for repeated clicking.\n if (modalopen) {\n modal.hide();\n return;\n }\n\n // Show modal.\n await modal.show();\n modalopen = true;\n const body = document.querySelector('body');\n body.classList.add(MODAL_OPEN);\n\n // Add listener for input submission.\n const textarea = document.getElementById('block_ai_chat-input-id');\n addTextareaListener(textarea);\n const button = document.getElementById('block_ai_chat-submit-id');\n button.addEventListener(\"click\", (event) => {\n clickSubmitButton(event);\n });\n\n if (firstLoad) {\n // Load conversations.\n await getConversations();\n\n // Show conversation.\n // Todo - Evtl. noch firstload verschönern, spinner für header und content z.b.\n showConversation();\n\n // Get conversationcontext message limit.\n let conversationcontextLimit = await externalServices.getConversationcontextLimit(contextid);\n maxHistory = conversationcontextLimit.limit;\n\n // Add listeners for dropdownmenus.\n // Actions.\n const btnNewDialog = document.getElementById('block_ai_chat_new_dialog');\n btnNewDialog.addEventListener('click', () => {\n newDialog();\n });\n const btnDeleteDialog = document.getElementById('block_ai_chat_delete_dialog');\n btnDeleteDialog.addEventListener('click', () => {\n deleteCurrentDialog();\n });\n const btnShowHistory = document.getElementById('block_ai_chat_show_history');\n btnShowHistory.addEventListener('click', () => {\n showHistory();\n });\n // Views.\n const btnChatwindow = document.getElementById(VIEW_CHATWINDOW);\n btnChatwindow.addEventListener('click', () => {\n setView(VIEW_CHATWINDOW);\n });\n const btnFullWidth = document.getElementById(VIEW_OPENFULL);\n btnFullWidth.addEventListener('click', () => {\n setView(VIEW_OPENFULL);\n });\n const btnDockRight = document.getElementById(VIEW_DOCKRIGHT);\n btnDockRight.addEventListener('click', () => {\n setView(VIEW_DOCKRIGHT);\n });\n\n // Show userquota.\n await renderUserQuota('#block_ai_chat_userquota', ['chat']);\n // Show infobox.\n await renderInfoBox('block_ai_chat', userid, '.ai_chat_modal_body [data-content=\"local_ai_manager_infobox\"]', ['chat']);\n\n // Check if all permissions and settings are correct.\n const message = await userAllowed();\n if (message !== '') {\n const notice = await getString('notice', 'block_ai_chat');\n await displayAlert(notice, message);\n }\n firstLoad = false;\n }\n\n helper.focustextarea();\n}\n\n\n/**\n * Webservice Get all conversations.\n */\nconst getConversations = async() => {\n try {\n allConversations = await externalServices.getAllConversations(userid, contextid);\n } catch (error) {\n displayException(error);\n }\n};\n\n/**\n * Function to set conversation.\n * @param {*} id\n */\nconst showConversation = (id = 0) => {\n // Dissallow changing conversations when question running.\n if (aiAtWork) {\n return;\n }\n // Change conversation or get last conversation.\n if (id !== 0) {\n // Set selected conversation.\n conversation = allConversations.find(x => x.id === id);\n } else if (typeof allConversations[0] !== 'undefined') {\n // Set last conversation.\n conversation = allConversations.at(allConversations.length - 1);\n } else if (allConversations.length === 0) {\n // Last conversation has been deleted.\n newDialog(true);\n }\n clearMessages();\n setModalHeader();\n showMessages();\n};\n// Make globally accessible since it is used to show history in dropdownmenuitem.mustache.\ndocument.showConversation = showConversation;\n\n\n/**\n * Send input to ai connector.\n * @param {*} question\n */\nconst enterQuestion = async(question) => {\n\n // Deny changing dialogs until answer present?\n if (question == '') {\n aiAtWork = false;\n return;\n }\n const message = await userAllowed();\n if (message !== '') {\n const notice = await getString('noticenewquestion', 'block_ai_chat');\n await displayAlert(notice, message);\n aiAtWork = false;\n return;\n }\n\n // Add to conversation, answer not yet available.\n showMessage(question, 'self', false);\n\n // For first message, add a system message.\n if (conversation.messages.length === 0) {\n const currentUserLanguage = Config.language.substring(0, 2);\n const LangNames = new Intl.DisplayNames('en', {type: 'language'});\n conversation.messages.push({\n 'message': 'Answer in ' + LangNames.of(currentUserLanguage),\n 'sender': 'system',\n });\n }\n\n // Ceck history for length limit.\n const convHistory = await checkMessageHistoryLengthLimit(conversation.messages);\n\n // Options, with conversation history.\n const options = {\n 'component': 'block_ai_chat',\n 'contextid': contextid,\n 'conversationcontext': convHistory,\n };\n\n // For a new conversation, get an id.\n if (conversation.id === 0) {\n try {\n let idresult = await externalServices.getNewConversationId(contextid);\n conversation.id = idresult.id;\n conversation.timecreated = Math.floor(Date.now() / 1000);\n setModalHeader(escapeHTML(question));\n } catch (error) {\n displayException(error);\n }\n options.forcenewitemid = true;\n }\n\n // Pass itemid / conversationid.\n options.itemid = conversation.id;\n\n // Send to local_ai_manager.\n let requestresult = await manager.askLocalAiManager('chat', question, options);\n\n // Handle errors.\n if (requestresult.code != 200) {\n requestresult = await errorHandling(requestresult, question, options);\n }\n\n // Attach copy listener.\n let copy = document.querySelector('.ai_chat_modal .awaitanswer .copy');\n copy.addEventListener('mousedown', () => {\n helper.copyToClipboard(copy);\n });\n\n // Write back answer.\n showReply(requestresult.result);\n\n // Ai is done.\n aiAtWork = false;\n\n // Save new question and answer.\n saveConversationLocally(question, requestresult.result);\n\n // Update userquota.\n const userquota = document.getElementById('block_ai_chat_userquota');\n userquota.innerHTML = '';\n renderUserQuota('#block_ai_chat_userquota', ['chat']);\n};\n\n/**\n * Render reply.\n * @param {string} text\n */\nconst showReply = async(text) => {\n // Get textblock.\n let fields = document.querySelectorAll('.ai_chat_modal .awaitanswer .text');\n const field = fields[fields.length - 1];\n // Render the reply.\n field.innerHTML = text;\n field.classList.remove('small');\n\n // Remove awaitanswer class.\n let awaitdivs = document.querySelectorAll('.ai_chat_modal .awaitanswer');\n const awaitdiv = awaitdivs[awaitdivs.length - 1];\n awaitdiv.classList.remove('awaitanswer');\n};\n\nconst showMessages = () => {\n conversation.messages.forEach((val) => {\n showMessage(val.message, val.sender);\n });\n};\n\n/**\n * Show answer from local_ai_manager.\n * @param {*} text\n * @param {*} sender User or Ai\n * @param {*} answer Is answer in history\n */\nconst showMessage = async(text, sender = '', answer = true) => {\n // Skip if sender is system.\n if (sender === 'system') {\n return;\n }\n // Imitate bool for message.mustache logic {{#sender}}.\n if (sender === 'ai') {\n sender = '';\n }\n // Escape chars for immediate rendering.\n if (!answer) {\n text = escapeHTML(text);\n }\n\n const templateData = {\n \"sender\": sender,\n \"content\": text,\n \"answer\": answer,\n };\n // Call the function to load and render our template.\n const {html, js} = await Templates.renderForPromise('block_ai_chat/message', templateData);\n Templates.appendNodeContents('.block_ai_chat-output', html, js);\n\n // Add copy listener for replys.\n if (sender === '') {\n helper.attachCopyListenerLast();\n }\n\n // Scroll the modal content to the bottom.\n helper.scrollToBottom();\n};\n\n/**\n * Create new / Reset dialog.\n * @param {bool} deleted\n */\nconst newDialog = async(deleted = false) => {\n if (aiAtWork) {\n return;\n }\n // Add current convo local representation, if not already there.\n if (allConversations.find(x => x.id === conversation.id) === undefined && !deleted) {\n allConversations.push(conversation);\n }\n // Reset local conservation.\n conversation = {\n id: 0,\n messages: [],\n };\n clearMessages();\n setModalHeader(strNewDialog);\n helper.focustextarea();\n};\n\n/**\n * Delete /hide current dialog.\n */\nconst deleteCurrentDialog = () => {\n deleteCancelPromise(\n getString('delete', 'block_ai_chat'),\n getString('deletewarning', 'block_ai_chat'),\n ).then(async() => {\n if (conversation.id !== 0) {\n try {\n const deleted = await externalServices.deleteConversation(contextid, userid, conversation.id);\n if (deleted) {\n removeFromHistory();\n showConversation();\n }\n } catch (error) {\n displayException(error);\n }\n }\n return;\n }).catch(() => {\n return;\n });\n};\n\n/**\n * Show conversation history.\n */\nconst showHistory = async() => {\n // Add current convo local representation, if not already there.\n if (allConversations.find(x => x.id === conversation.id) === undefined) {\n allConversations.push(conversation);\n }\n // Change title and add backlink.\n let title = '' + strHistory + '';\n clearMessages(true);\n setModalHeader(title);\n const btnBacklink = document.getElementById('block_ai_chat_backlink');\n btnBacklink.addEventListener('click', () => {\n if (conversation.id !== 0) {\n showConversation(conversation.id);\n } else {\n newDialog();\n }\n clearMessages();\n setModalHeader();\n });\n\n // Set modal class to hide info about ratelimits and infobox.\n let modal = document.querySelector('.ai_chat_modal');\n modal.classList.add('onhistorypage');\n\n // Iterate over conversations and group by date.\n let groupedByDate = {};\n allConversations.forEach((convo) => {\n if (typeof convo.messages[1] !== 'undefined') {\n // Get first prompt.\n let title = convo.messages[1].message;\n\n // Get date and sort convos into a date array.\n const now = new Date();\n const date = new Date(convo.timecreated * 1000);\n const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());\n const yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);\n const twoWeeksAgo = new Date(now);\n twoWeeksAgo.setDate(now.getDate() - 14);\n\n const options = {weekday: 'long', day: '2-digit', month: '2-digit'};\n const monthOptions = {month: 'long', year: '2-digit'};\n\n // Create a date string.\n let dateString = '';\n if (date >= today) {\n dateString = strToday;\n } else if (date >= yesterday) {\n dateString = strYesterday;\n } else if (date >= twoWeeksAgo) {\n dateString = date.toLocaleDateString(undefined, options);\n } else {\n dateString = date.toLocaleDateString(undefined, monthOptions);\n }\n\n // Create a time string.\n const hours = date.getHours();\n const minutes = date.getMinutes().toString().padStart(2, '0');\n\n let convItem = {\n \"title\": title,\n \"conversationid\": convo.id,\n \"time\": hours + ':' + minutes,\n };\n\n // Save entry under the date.\n if (!groupedByDate[dateString]) {\n groupedByDate[dateString] = [];\n }\n groupedByDate[dateString].push(convItem);\n }\n });\n\n // Convert the grouped objects into an array format that Mustache can iterate over.\n let convert = {\n groups: Object.keys(groupedByDate).map(key => ({\n key: key,\n objects: groupedByDate[key]\n }))\n };\n\n // Render history.\n const templateData = {\n \"dates\": convert.groups,\n };\n const {html, js} = await Templates.renderForPromise('block_ai_chat/history', templateData);\n Templates.appendNodeContents('.ai_chat_modal .block_ai_chat-output', html, js);\n\n // Add a listener for the new dialog button.\n const btnNewDialog = document.getElementById('ai_chat_history_new_dialog');\n btnNewDialog.addEventListener('mousedown', () => {\n newDialog();\n });\n};\n\n/**\n * Remove currrent conversation from history.\n */\nconst removeFromHistory = () => {\n // Cant remove if new or not yet in history.\n if (conversation.id !== 0 && allConversations.find(x => x.id === conversation.id) !== undefined) {\n // Build new allConversations array without deleted one.\n allConversations = allConversations.filter(obj => obj.id !== conversation.id);\n }\n};\n\n/**\n * Webservice Save conversation.\n * @param {*} question\n * @param {*} reply\n */\nconst saveConversationLocally = (question, reply) => {\n // Add to local representation.\n let message = {'message': question, 'sender': 'user'};\n conversation.messages.push(message);\n message = {'message': reply, 'sender': 'ai'};\n conversation.messages.push(message);\n};\n\n/**\n * Clear output div.\n * @param {*} hideinput\n */\nconst clearMessages = (hideinput = false) => {\n const output = document.querySelector('.block_ai_chat-output');\n output.innerHTML = '';\n // For showing history.\n let input = document.querySelector('.block_ai_chat-input');\n if (hideinput) {\n input.style.display = 'none';\n } else {\n input.style.display = 'flex';\n }\n};\n\n/**\n * Set modal header title.\n * @param {*} setTitle\n */\nconst setModalHeader = (setTitle = '') => {\n let modalheader = document.querySelector('.ai_chat_modal .modal-title div');\n let title = '';\n if (modalheader !== null && (conversation.messages.length > 0 || setTitle.length)) {\n if (!setTitle.length) {\n title = conversation.messages[1].message;\n } else {\n title = setTitle;\n }\n modalheader.innerHTML = title;\n }\n // Remove onhistorypage, since history page is setting it.\n let modal = document.querySelector('.ai_chat_modal');\n modal.classList.remove('onhistorypage');\n};\n\n/**\n * Attach event listener.\n * @param {*} textarea\n */\nconst addTextareaListener = (textarea) => {\n textarea.addEventListener('keydown', (event) => {\n // Handle submission.\n textareaOnKeydown(event);\n\n // Handle autgrow.\n // Reset the height to auto to get the correct scrollHeight.\n textarea.style.height = 'auto';\n\n // Fetch the computed styles.\n const computedStyles = window.getComputedStyle(textarea);\n const lineHeight = parseFloat(computedStyles.lineHeight);\n const paddingTop = parseFloat(computedStyles.paddingTop);\n const paddingBottom = parseFloat(computedStyles.paddingBottom);\n const borderTop = parseFloat(computedStyles.borderTopWidth);\n const borderBottom = parseFloat(computedStyles.borderBottomWidth);\n\n // Calculate the maximum height for four rows plus padding and borders.\n const maxHeight = (lineHeight * 4) + paddingTop + paddingBottom + borderTop + borderBottom;\n\n // Calculate the new height based on the scrollHeight.\n const newHeight = Math.min(textarea.scrollHeight + borderTop + borderBottom, maxHeight);\n\n // Set the new height.\n textarea.style.height = newHeight + 'px';\n });\n};\n\n/**\n * Action for textarea submission.\n * @param {*} event\n */\nconst textareaOnKeydown = (event) => {\n // TODO check for mobile devices.\n if (event.key === 'Enter' && !aiAtWork && !event.shiftKey) {\n aiAtWork = true;\n enterQuestion(event.target.value);\n event.preventDefault();\n event.target.value = '';\n }\n};\n\n/**\n * Submit form.\n */\nconst clickSubmitButton = () => {\n // Var aiAtWork to make it impossible to submit multiple questions at once.\n if (!aiAtWork) {\n aiAtWork = true;\n const textarea = document.getElementById('block_ai_chat-input-id');\n enterQuestion(textarea.value);\n textarea.value = '';\n }\n};\n\n/**\n * Handle error from local_ai_manager.\n * @param {*} requestresult\n * @param {*} question\n * @param {*} options\n * @returns {object}\n */\nconst errorHandling = async(requestresult, question, options) => {\n\n // If code 409, conversationid is already taken, try get new a one.\n if (requestresult.code == 409) {\n while (requestresult.code == 409) {\n try {\n let idresult = await externalServices.getNewConversationId(contextid);\n conversation.id = idresult.id;\n options.itemid = conversation.id;\n } catch (error) {\n displayException(error);\n }\n // Retry with new id.\n requestresult = await manager.askLocalAiManager('chat', question, options);\n return requestresult;\n }\n }\n\n // If any other errorcode, alert with errormessage.\n const errorString = await getString('errorwithcode', 'block_ai_chat', requestresult.code);\n const result = JSON.parse(requestresult.result);\n await displayAlert(errorString, result.message);\n\n // Change answer styling to differentiate from ai.\n const answerdivs = document.querySelectorAll('.awaitanswer');\n const answerdiv = answerdivs[answerdivs.length - 1];\n const messagediv = answerdiv.closest('.message');\n messagediv.classList.add('text-danger');\n\n // And write generic error message in chatbot.\n requestresult.result = await getString('error', 'block_ai_chat');\n return requestresult;\n};\n\n/**\n * Check historic messages for max length.\n * @param {array} messages\n * @returns {array}\n */\nconst checkMessageHistoryLengthLimit = async(messages) => {\n const length = messages.length;\n if (length > maxHistory) {\n // Cut history.\n let shortenedMessages = [messages[0], ...messages.slice(-maxHistory)];\n\n // Show warning once per session.\n if (!maxHistoryWarnings.has(conversation.id)) {\n const maxHistoryString = await getString('maxhistory', 'block_ai_chat', maxHistory);\n const warningErrorString = await getString('maxhistoryreached', 'block_ai_chat', maxHistory);\n await displayAlert(maxHistoryString, warningErrorString);\n // Remember warning.\n maxHistoryWarnings.add(conversation.id);\n }\n return shortenedMessages;\n }\n // Limit not reached, return messages.\n return messages;\n};\n\n/**\n * Check if modal should close on outside click.\n * @param {*} event\n */\nconst checkOutsideClick = (event) => {\n // View openfull acts like a normal modal.\n if (viewmode != VIEW_OPENFULL) {\n event.preventDefault();\n }\n};\n\n/**\n * Set different viewmodes and save in local storage.\n * @param {string} mode\n */\nconst setView = async(mode = '') => {\n const key = await hash('chatmode' + userid);\n // Check for saved viewmode.\n let savedmode = LocalStorage.get(key);\n if (mode == '') {\n if (!savedmode) {\n // Set default.\n mode = VIEW_CHATWINDOW;\n } else {\n mode = savedmode;\n }\n }\n // Save viewmode and set global var.\n LocalStorage.set(key, mode);\n viewmode = mode;\n\n // Set viewmode as bodyclass.\n const body = document.querySelector('body');\n body.classList.remove(VIEW_CHATWINDOW, VIEW_OPENFULL, VIEW_DOCKRIGHT);\n body.classList.add(mode);\n};\n\n/**\n * Is user allowed new queries.\n * @returns {message}\n */\nconst userAllowed = async() => {\n let message;\n if (tenantConfig.tenantenabled === false) {\n message = await getString('error_http403disabled', 'local_ai_manager');\n return message;\n }\n if (tenantConfig.userconfirmed === false) {\n message = await getString('error_http403notconfirmed', 'local_ai_manager');\n message += \". \";\n const link = window.location.origin + '/local/ai_manager/confirm_ai_usage.php';\n message += await getString('confirm_ai_usage', 'block_ai_chat', link);\n return message;\n }\n if (tenantConfig.userlocked === true) {\n message = await getString('error_http403blocked', 'local_ai_manager');\n return message;\n }\n if (chatConfig.isconfigured === false) {\n message = await getString('error_purposenotconfigured', 'local_ai_manager');\n return message;\n }\n if (chatConfig.lockedforrole === true) {\n message = await getString('error_http403blocked', 'local_ai_manager');\n return message;\n }\n if (chatConfig.limitreached === true) {\n message = await getString('error_limitreached', 'local_ai_manager');\n return message;\n }\n return '';\n};\n\n/**\n * Change to openfull view when screen is small.\n * @param {*} e\n */\nconst handleScreenWidthChange = (e) => {\n const body = document.querySelector('body');\n if (e.matches) {\n // Screen width is less than 576px\n body.classList.remove(VIEW_CHATWINDOW, VIEW_OPENFULL, VIEW_DOCKRIGHT);\n body.classList.add(VIEW_OPENFULL);\n } else {\n body.classList.remove(VIEW_CHATWINDOW, VIEW_OPENFULL, VIEW_DOCKRIGHT);\n body.classList.add(viewmode);\n }\n};\n"],"names":["VIEW_CHATWINDOW","VIEW_OPENFULL","VIEW_DOCKRIGHT","strHistory","strNewDialog","strToday","strYesterday","badge","viewmode","modal","modalopen","conversation","id","messages","allConversations","userid","contextid","firstLoad","aiAtWork","maxHistory","maxHistoryWarnings","Set","tenantConfig","chatConfig","DialogModal","Modal","configure","modalConfig","show","removeOnClose","isVerticallyCentered","titletest","setTitletest","value","hide","document","querySelector","classList","remove","async","params","new","history","aiConfig","purposes","find","p","purpose","create","templateContext","title","getRoot","on","e","target","add","ModalEvents","outsideClick","event","checkOutsideClick","setView","getElementById","addEventListener","textarea","addTextareaListener","clickSubmitButton","getConversations","showConversation","conversationcontextLimit","externalServices","getConversationcontextLimit","limit","newDialog","deleteCurrentDialog","showHistory","message","userAllowed","notice","helper","focustextarea","showModal","window","matchMedia","handleScreenWidthChange","innerWidth","getAllConversations","error","x","at","length","clearMessages","setModalHeader","showMessages","enterQuestion","question","showMessage","currentUserLanguage","Config","language","substring","LangNames","Intl","DisplayNames","type","push","of","convHistory","checkMessageHistoryLengthLimit","options","idresult","getNewConversationId","timecreated","Math","floor","Date","now","forcenewitemid","itemid","requestresult","manager","askLocalAiManager","code","errorHandling","copy","copyToClipboard","showReply","result","saveConversationLocally","innerHTML","fields","querySelectorAll","field","text","awaitdivs","forEach","val","sender","answer","templateData","html","js","Templates","renderForPromise","appendNodeContents","attachCopyListenerLast","scrollToBottom","deleted","undefined","then","deleteConversation","removeFromHistory","catch","groupedByDate","convo","date","today","getFullYear","getMonth","getDate","yesterday","twoWeeksAgo","setDate","weekday","day","month","monthOptions","year","dateString","toLocaleDateString","hours","getHours","minutes","getMinutes","toString","padStart","convItem","groups","Object","keys","map","key","objects","filter","obj","reply","hideinput","output","input","style","display","setTitle","modalheader","textareaOnKeydown","height","computedStyles","getComputedStyle","lineHeight","parseFloat","paddingTop","paddingBottom","borderTop","borderTopWidth","borderBottom","borderBottomWidth","maxHeight","newHeight","min","scrollHeight","shiftKey","preventDefault","errorString","JSON","parse","answerdivs","closest","shortenedMessages","slice","has","maxHistoryString","warningErrorString","mode","savedmode","LocalStorage","get","set","body","tenantenabled","userconfirmed","link","location","origin","userlocked","isconfigured","lockedforrole","limitreached","matches"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"dialog.min.js","sources":["../src/dialog.js"],"sourcesContent":["import Modal from 'core/modal';\nimport * as externalServices from 'block_ai_chat/webservices';\nimport Templates from 'core/templates';\nimport {alert as displayAlert, exception as displayException, deleteCancelPromise} from 'core/notification';\nimport ModalEvents from 'core/modal_events';\nimport * as helper from 'block_ai_chat/helper';\nimport * as manager from 'block_ai_chat/ai_manager';\nimport {getString} from 'core/str';\nimport {renderInfoBox} from 'local_ai_manager/infobox';\nimport {renderUserQuota} from 'local_ai_manager/userquota';\nimport {getAiConfig} from 'local_ai_manager/config';\nimport LocalStorage from 'core/localstorage';\nimport {escapeHTML, hash} from './helper';\nimport Config from 'core/config';\n\n// Declare variables.\nconst VIEW_CHATWINDOW = 'block_ai_chat_chatwindow';\nconst VIEW_OPENFULL = 'block_ai_chat_openfull';\nconst VIEW_DOCKRIGHT = 'block_ai_chat_dockright';\nconst MODAL_OPEN = 'block_ai_chat_open';\n\n// Modal.\nlet modal = {};\nlet strHistory;\nlet strNewDialog;\nlet strToday;\nlet strYesterday;\nlet badge;\nlet viewmode;\nlet modalopen = false;\n\n// Current conversation.\nlet conversation = {\n id: 0,\n messages: [],\n};\n// All conversations.\nlet allConversations = [];\n// Userid.\nlet userid = 0;\n// Course context id.\nlet contextid = 0;\n// First load.\nlet firstLoad = true;\n// AI in process of answering.\nlet aiAtWork = false;\n// Maximum history included in query.\nlet maxHistory = 5;\n// Remember warnings for maximum history in this session.\nlet maxHistoryWarnings = new Set();\n// Tenantconfig.\nlet tenantConfig = {};\nlet chatConfig = {};\n\nclass DialogModal extends Modal {\n static TYPE = \"block_ai_chat/dialog_modal\";\n static TEMPLATE = \"block_ai_chat/dialog_modal\";\n\n configure(modalConfig) {\n // Show this modal on instantiation.\n modalConfig.show = false;\n\n // Remove from the DOM on close.\n modalConfig.removeOnClose = false;\n\n modalConfig.isVerticallyCentered = false;\n\n super.configure(modalConfig);\n\n // Accept our own custom arguments too.\n if (modalConfig.titletest) {\n this.setTitletest(modalConfig.titletest);\n }\n }\n\n setTitletest(value) {\n this.titletest = value;\n }\n\n hide() {\n super.hide();\n // Keep track of state, to restrict changes to block_ai_chat modal.\n modalopen = false;\n const body = document.querySelector('body');\n body.classList.remove(MODAL_OPEN);\n }\n}\n\nexport const init = async(params) => {\n // Read params.\n userid = params.userid;\n contextid = params.contextid;\n strNewDialog = params.new;\n strHistory = params.history;\n badge = params.badge;\n // Disable bdage.\n badge = false;\n\n // Get configuration.\n const aiConfig = await getAiConfig();\n tenantConfig = aiConfig;\n chatConfig = aiConfig.purposes.find(p => p.purpose === \"chat\");\n\n // Build modal.\n modal = await DialogModal.create({\n templateContext: {\n title: strNewDialog,\n badge: badge,\n },\n });\n\n // Add class for styling when modal is displayed.\n modal.getRoot().on('modal:shown', function(e) {\n e.target.classList.add(\"block_ai_chat_modal\");\n });\n\n // Conditionally prevent outside click event.\n modal.getRoot().on(ModalEvents.outsideClick, event => {\n checkOutsideClick(event);\n });\n\n // Check and set viewmode.\n setView();\n\n // Attach listener to the ai button to call modal.\n let button = document.getElementById('ai_chat_button');\n button.addEventListener('mousedown', async() => {\n showModal(params);\n });\n\n // Get strings.\n strToday = await getString('today', 'core');\n strYesterday = await getString('yesterday', 'block_ai_chat');\n\n // Create a MediaQueryList object to check for small screens.\n const mediaQuery = window.matchMedia(\"(max-width: 576px)\");\n\n // Attach the event listener to handle changes.\n mediaQuery.addEventListener('change', handleScreenWidthChange);\n\n // Initial check for screenwidth.\n if (window.innerWidth <= 576) {\n setView(VIEW_OPENFULL);\n }\n};\n\n/**\n * Show ai_chat modal.\n */\nasync function showModal() {\n // Switch for repeated clicking.\n if (modalopen) {\n modal.hide();\n return;\n }\n\n // Show modal.\n await modal.show();\n modalopen = true;\n const body = document.querySelector('body');\n body.classList.add(MODAL_OPEN);\n\n // Add listener for input submission.\n const textarea = document.getElementById('block_ai_chat-input-id');\n addTextareaListener(textarea);\n const button = document.getElementById('block_ai_chat-submit-id');\n button.addEventListener(\"click\", (event) => {\n clickSubmitButton(event);\n });\n\n if (firstLoad) {\n // Load conversations.\n await getConversations();\n\n // Show conversation.\n // Todo - Evtl. noch firstload verschönern, spinner für header und content z.b.\n showConversation();\n\n // Get conversationcontext message limit.\n let conversationcontextLimit = await externalServices.getConversationcontextLimit(contextid);\n maxHistory = conversationcontextLimit.limit;\n\n // Add listeners for dropdownmenus.\n // Actions.\n const btnNewDialog = document.getElementById('block_ai_chat_new_dialog');\n btnNewDialog.addEventListener('click', () => {\n newDialog();\n });\n const btnDeleteDialog = document.getElementById('block_ai_chat_delete_dialog');\n btnDeleteDialog.addEventListener('click', () => {\n deleteCurrentDialog();\n });\n const btnShowHistory = document.getElementById('block_ai_chat_show_history');\n btnShowHistory.addEventListener('click', () => {\n showHistory();\n });\n // Views.\n const btnChatwindow = document.getElementById(VIEW_CHATWINDOW);\n btnChatwindow.addEventListener('click', () => {\n setView(VIEW_CHATWINDOW);\n });\n const btnFullWidth = document.getElementById(VIEW_OPENFULL);\n btnFullWidth.addEventListener('click', () => {\n setView(VIEW_OPENFULL);\n });\n const btnDockRight = document.getElementById(VIEW_DOCKRIGHT);\n btnDockRight.addEventListener('click', () => {\n setView(VIEW_DOCKRIGHT);\n });\n\n // Show userquota.\n await renderUserQuota('#block_ai_chat_userquota', ['chat']);\n // Show infobox.\n await renderInfoBox(\n 'block_ai_chat', userid, '.block_ai_chat_modal_body [data-content=\"local_ai_manager_infobox\"]', ['chat']\n );\n\n // Check if all permissions and settings are correct.\n const message = await userAllowed();\n if (message !== '') {\n const notice = await getString('notice', 'block_ai_chat');\n await displayAlert(notice, message);\n }\n firstLoad = false;\n }\n\n helper.focustextarea();\n}\n\n\n/**\n * Webservice Get all conversations.\n */\nconst getConversations = async() => {\n try {\n allConversations = await externalServices.getAllConversations(userid, contextid);\n } catch (error) {\n displayException(error);\n }\n};\n\n/**\n * Function to set conversation.\n * @param {*} id\n */\nconst showConversation = (id = 0) => {\n // Dissallow changing conversations when question running.\n if (aiAtWork) {\n return;\n }\n // Change conversation or get last conversation.\n if (id !== 0) {\n // Set selected conversation.\n conversation = allConversations.find(x => x.id === id);\n } else if (typeof allConversations[0] !== 'undefined') {\n // Set last conversation.\n conversation = allConversations.at(allConversations.length - 1);\n } else if (allConversations.length === 0) {\n // Last conversation has been deleted.\n newDialog(true);\n }\n clearMessages();\n setModalHeader();\n showMessages();\n};\n// Make globally accessible since it is used to show history in dropdownmenuitem.mustache.\ndocument.showConversation = showConversation;\n\n\n/**\n * Send input to ai connector.\n * @param {*} question\n */\nconst enterQuestion = async(question) => {\n\n // Deny changing dialogs until answer present?\n if (question == '') {\n aiAtWork = false;\n return;\n }\n const message = await userAllowed();\n if (message !== '') {\n const notice = await getString('noticenewquestion', 'block_ai_chat');\n await displayAlert(notice, message);\n aiAtWork = false;\n return;\n }\n\n // Add to conversation, answer not yet available.\n showMessage(question, 'self', false);\n\n // For first message, add a system message.\n if (conversation.messages.length === 0) {\n const currentUserLanguage = Config.language.substring(0, 2);\n const LangNames = new Intl.DisplayNames('en', {type: 'language'});\n conversation.messages.push({\n 'message': 'Answer in ' + LangNames.of(currentUserLanguage),\n 'sender': 'system',\n });\n }\n\n // Ceck history for length limit.\n const convHistory = await checkMessageHistoryLengthLimit(conversation.messages);\n\n // Options, with conversation history.\n const options = {\n 'component': 'block_ai_chat',\n 'contextid': contextid,\n 'conversationcontext': convHistory,\n };\n\n // For a new conversation, get an id.\n if (conversation.id === 0) {\n try {\n let idresult = await externalServices.getNewConversationId(contextid);\n conversation.id = idresult.id;\n conversation.timecreated = Math.floor(Date.now() / 1000);\n setModalHeader(escapeHTML(question));\n } catch (error) {\n displayException(error);\n }\n options.forcenewitemid = true;\n }\n\n // Pass itemid / conversationid.\n options.itemid = conversation.id;\n\n // Send to local_ai_manager.\n let requestresult = await manager.askLocalAiManager('chat', question, options);\n\n // Handle errors.\n if (requestresult.code != 200) {\n requestresult = await errorHandling(requestresult, question, options);\n }\n\n // Attach copy listener.\n let copy = document.querySelector('.block_ai_chat_modal .awaitanswer .copy');\n copy.addEventListener('mousedown', () => {\n helper.copyToClipboard(copy);\n });\n\n // Write back answer.\n showReply(requestresult.result);\n\n // Ai is done.\n aiAtWork = false;\n\n // Save new question and answer.\n saveConversationLocally(question, requestresult.result);\n\n // Update userquota.\n const userquota = document.getElementById('block_ai_chat_userquota');\n userquota.innerHTML = '';\n renderUserQuota('#block_ai_chat_userquota', ['chat']);\n};\n\n/**\n * Render reply.\n * @param {string} text\n */\nconst showReply = async(text) => {\n // Get textblock.\n let fields = document.querySelectorAll('.block_ai_chat_modal .awaitanswer .text');\n const field = fields[fields.length - 1];\n // Render the reply.\n field.innerHTML = text;\n field.classList.remove('small');\n\n // Remove awaitanswer class.\n let awaitdivs = document.querySelectorAll('.block_ai_chat_modal .awaitanswer');\n const awaitdiv = awaitdivs[awaitdivs.length - 1];\n awaitdiv.classList.remove('awaitanswer');\n};\n\nconst showMessages = () => {\n conversation.messages.forEach((val) => {\n showMessage(val.message, val.sender);\n });\n};\n\n/**\n * Show answer from local_ai_manager.\n * @param {*} text\n * @param {*} sender User or Ai\n * @param {*} answer Is answer in history\n */\nconst showMessage = async(text, sender = '', answer = true) => {\n // Skip if sender is system.\n if (sender === 'system') {\n return;\n }\n // Imitate bool for message.mustache logic {{#sender}}.\n if (sender === 'ai') {\n sender = '';\n }\n // Escape chars for immediate rendering.\n if (!answer) {\n text = escapeHTML(text);\n }\n\n const templateData = {\n \"sender\": sender,\n \"content\": text,\n \"answer\": answer,\n };\n // Call the function to load and render our template.\n const {html, js} = await Templates.renderForPromise('block_ai_chat/message', templateData);\n Templates.appendNodeContents('.block_ai_chat-output', html, js);\n\n // Add copy listener for replys.\n if (sender === '') {\n helper.attachCopyListenerLast();\n }\n\n // Scroll the modal content to the bottom.\n helper.scrollToBottom();\n};\n\n/**\n * Create new / Reset dialog.\n * @param {bool} deleted\n */\nconst newDialog = async(deleted = false) => {\n if (aiAtWork) {\n return;\n }\n // Add current convo local representation, if not already there.\n if (allConversations.find(x => x.id === conversation.id) === undefined && !deleted) {\n allConversations.push(conversation);\n }\n // Reset local conservation.\n conversation = {\n id: 0,\n messages: [],\n };\n clearMessages();\n setModalHeader(strNewDialog);\n helper.focustextarea();\n};\n\n/**\n * Delete /hide current dialog.\n */\nconst deleteCurrentDialog = () => {\n deleteCancelPromise(\n getString('delete', 'block_ai_chat'),\n getString('deletewarning', 'block_ai_chat'),\n ).then(async() => {\n if (conversation.id !== 0) {\n try {\n const deleted = await externalServices.deleteConversation(contextid, userid, conversation.id);\n if (deleted) {\n removeFromHistory();\n showConversation();\n }\n } catch (error) {\n displayException(error);\n }\n }\n return;\n }).catch(() => {\n return;\n });\n};\n\n/**\n * Show conversation history.\n */\nconst showHistory = async() => {\n // Add current convo local representation, if not already there.\n if (allConversations.find(x => x.id === conversation.id) === undefined) {\n allConversations.push(conversation);\n }\n // Change title and add backlink.\n let title = '' + strHistory + '';\n clearMessages(true);\n setModalHeader(title);\n const btnBacklink = document.getElementById('block_ai_chat_backlink');\n btnBacklink.addEventListener('click', () => {\n if (conversation.id !== 0) {\n showConversation(conversation.id);\n } else {\n newDialog();\n }\n clearMessages();\n setModalHeader();\n });\n\n // Set modal class to hide info about ratelimits and infobox.\n let modal = document.querySelector('.block_ai_chat_modal');\n modal.classList.add('onhistorypage');\n\n // Iterate over conversations and group by date.\n let groupedByDate = {};\n allConversations.forEach((convo) => {\n if (typeof convo.messages[1] !== 'undefined') {\n // Get first prompt.\n let title = convo.messages[1].message;\n\n // Get date and sort convos into a date array.\n const now = new Date();\n const date = new Date(convo.timecreated * 1000);\n const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());\n const yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);\n const twoWeeksAgo = new Date(now);\n twoWeeksAgo.setDate(now.getDate() - 14);\n\n const options = {weekday: 'long', day: '2-digit', month: '2-digit'};\n const monthOptions = {month: 'long', year: '2-digit'};\n\n // Create a date string.\n let dateString = '';\n if (date >= today) {\n dateString = strToday;\n } else if (date >= yesterday) {\n dateString = strYesterday;\n } else if (date >= twoWeeksAgo) {\n dateString = date.toLocaleDateString(undefined, options);\n } else {\n dateString = date.toLocaleDateString(undefined, monthOptions);\n }\n\n // Create a time string.\n const hours = date.getHours();\n const minutes = date.getMinutes().toString().padStart(2, '0');\n\n let convItem = {\n \"title\": title,\n \"conversationid\": convo.id,\n \"time\": hours + ':' + minutes,\n };\n\n // Save entry under the date.\n if (!groupedByDate[dateString]) {\n groupedByDate[dateString] = [];\n }\n groupedByDate[dateString].push(convItem);\n }\n });\n\n // Convert the grouped objects into an array format that Mustache can iterate over.\n let convert = {\n groups: Object.keys(groupedByDate).map(key => ({\n key: key,\n objects: groupedByDate[key]\n }))\n };\n\n // Render history.\n const templateData = {\n \"dates\": convert.groups,\n };\n const {html, js} = await Templates.renderForPromise('block_ai_chat/history', templateData);\n Templates.appendNodeContents('.block_ai_chat_modal .block_ai_chat-output', html, js);\n\n // Add a listener for the new dialog button.\n const btnNewDialog = document.getElementById('ai_chat_history_new_dialog');\n btnNewDialog.addEventListener('mousedown', () => {\n newDialog();\n });\n};\n\n/**\n * Remove currrent conversation from history.\n */\nconst removeFromHistory = () => {\n // Cant remove if new or not yet in history.\n if (conversation.id !== 0 && allConversations.find(x => x.id === conversation.id) !== undefined) {\n // Build new allConversations array without deleted one.\n allConversations = allConversations.filter(obj => obj.id !== conversation.id);\n }\n};\n\n/**\n * Webservice Save conversation.\n * @param {*} question\n * @param {*} reply\n */\nconst saveConversationLocally = (question, reply) => {\n // Add to local representation.\n let message = {'message': question, 'sender': 'user'};\n conversation.messages.push(message);\n message = {'message': reply, 'sender': 'ai'};\n conversation.messages.push(message);\n};\n\n/**\n * Clear output div.\n * @param {*} hideinput\n */\nconst clearMessages = (hideinput = false) => {\n const output = document.querySelector('.block_ai_chat-output');\n output.innerHTML = '';\n // For showing history.\n let input = document.querySelector('.block_ai_chat-input');\n if (hideinput) {\n input.style.display = 'none';\n } else {\n input.style.display = 'flex';\n }\n};\n\n/**\n * Set modal header title.\n * @param {*} setTitle\n */\nconst setModalHeader = (setTitle = '') => {\n let modalheader = document.querySelector('.block_ai_chat_modal .modal-title div');\n let title = '';\n if (modalheader !== null && (conversation.messages.length > 0 || setTitle.length)) {\n if (!setTitle.length) {\n title = conversation.messages[1].message;\n } else {\n title = setTitle;\n }\n modalheader.innerHTML = title;\n }\n // Remove onhistorypage, since history page is setting it.\n let modal = document.querySelector('.block_ai_chat_modal');\n modal.classList.remove('onhistorypage');\n};\n\n/**\n * Attach event listener.\n * @param {*} textarea\n */\nconst addTextareaListener = (textarea) => {\n textarea.addEventListener('keydown', (event) => {\n // Handle submission.\n textareaOnKeydown(event);\n\n // Handle autgrow.\n // Reset the height to auto to get the correct scrollHeight.\n textarea.style.height = 'auto';\n\n // Fetch the computed styles.\n const computedStyles = window.getComputedStyle(textarea);\n const lineHeight = parseFloat(computedStyles.lineHeight);\n const paddingTop = parseFloat(computedStyles.paddingTop);\n const paddingBottom = parseFloat(computedStyles.paddingBottom);\n const borderTop = parseFloat(computedStyles.borderTopWidth);\n const borderBottom = parseFloat(computedStyles.borderBottomWidth);\n\n // Calculate the maximum height for four rows plus padding and borders.\n const maxHeight = (lineHeight * 4) + paddingTop + paddingBottom + borderTop + borderBottom;\n\n // Calculate the new height based on the scrollHeight.\n const newHeight = Math.min(textarea.scrollHeight + borderTop + borderBottom, maxHeight);\n\n // Set the new height.\n textarea.style.height = newHeight + 'px';\n });\n};\n\n/**\n * Action for textarea submission.\n * @param {*} event\n */\nconst textareaOnKeydown = (event) => {\n // TODO check for mobile devices.\n if (event.key === 'Enter' && !aiAtWork && !event.shiftKey) {\n aiAtWork = true;\n enterQuestion(event.target.value);\n event.preventDefault();\n event.target.value = '';\n }\n};\n\n/**\n * Submit form.\n */\nconst clickSubmitButton = () => {\n // Var aiAtWork to make it impossible to submit multiple questions at once.\n if (!aiAtWork) {\n aiAtWork = true;\n const textarea = document.getElementById('block_ai_chat-input-id');\n enterQuestion(textarea.value);\n textarea.value = '';\n }\n};\n\n/**\n * Handle error from local_ai_manager.\n * @param {*} requestresult\n * @param {*} question\n * @param {*} options\n * @returns {object}\n */\nconst errorHandling = async(requestresult, question, options) => {\n\n // If code 409, conversationid is already taken, try get new a one.\n if (requestresult.code == 409) {\n while (requestresult.code == 409) {\n try {\n let idresult = await externalServices.getNewConversationId(contextid);\n conversation.id = idresult.id;\n options.itemid = conversation.id;\n } catch (error) {\n displayException(error);\n }\n // Retry with new id.\n requestresult = await manager.askLocalAiManager('chat', question, options);\n return requestresult;\n }\n }\n\n // If any other errorcode, alert with errormessage.\n const errorString = await getString('errorwithcode', 'block_ai_chat', requestresult.code);\n const result = JSON.parse(requestresult.result);\n await displayAlert(errorString, result.message);\n\n // Change answer styling to differentiate from ai.\n const answerdivs = document.querySelectorAll('.awaitanswer');\n const answerdiv = answerdivs[answerdivs.length - 1];\n const messagediv = answerdiv.closest('.message');\n messagediv.classList.add('text-danger');\n\n // And write generic error message in chatbot.\n requestresult.result = await getString('error', 'block_ai_chat');\n return requestresult;\n};\n\n/**\n * Check historic messages for max length.\n * @param {array} messages\n * @returns {array}\n */\nconst checkMessageHistoryLengthLimit = async(messages) => {\n const length = messages.length;\n if (length > maxHistory) {\n // Cut history.\n let shortenedMessages = [messages[0], ...messages.slice(-maxHistory)];\n\n // Show warning once per session.\n if (!maxHistoryWarnings.has(conversation.id)) {\n const maxHistoryString = await getString('maxhistory', 'block_ai_chat', maxHistory);\n const warningErrorString = await getString('maxhistoryreached', 'block_ai_chat', maxHistory);\n await displayAlert(maxHistoryString, warningErrorString);\n // Remember warning.\n maxHistoryWarnings.add(conversation.id);\n }\n return shortenedMessages;\n }\n // Limit not reached, return messages.\n return messages;\n};\n\n/**\n * Check if modal should close on outside click.\n * @param {*} event\n */\nconst checkOutsideClick = (event) => {\n // View openfull acts like a normal modal.\n if (viewmode != VIEW_OPENFULL) {\n event.preventDefault();\n }\n};\n\n/**\n * Set different viewmodes and save in local storage.\n * @param {string} mode\n */\nconst setView = async(mode = '') => {\n const key = await hash('chatmode' + userid);\n // Check for saved viewmode.\n let savedmode = LocalStorage.get(key);\n if (mode == '') {\n if (!savedmode) {\n // Set default.\n mode = VIEW_CHATWINDOW;\n } else {\n mode = savedmode;\n }\n }\n // Save viewmode and set global var.\n LocalStorage.set(key, mode);\n viewmode = mode;\n\n // Set viewmode as bodyclass.\n const body = document.querySelector('body');\n body.classList.remove(VIEW_CHATWINDOW, VIEW_OPENFULL, VIEW_DOCKRIGHT);\n body.classList.add(mode);\n};\n\n/**\n * Is user allowed new queries.\n * @returns {message}\n */\nconst userAllowed = async() => {\n let message;\n if (tenantConfig.tenantenabled === false) {\n message = await getString('error_http403disabled', 'local_ai_manager');\n return message;\n }\n if (tenantConfig.userconfirmed === false) {\n message = await getString('error_http403notconfirmed', 'local_ai_manager');\n message += \". \";\n const link = window.location.origin + '/local/ai_manager/confirm_ai_usage.php';\n message += await getString('confirm_ai_usage', 'block_ai_chat', link);\n return message;\n }\n if (tenantConfig.userlocked === true) {\n message = await getString('error_http403blocked', 'local_ai_manager');\n return message;\n }\n if (chatConfig.isconfigured === false) {\n message = await getString('error_purposenotconfigured', 'local_ai_manager');\n return message;\n }\n if (chatConfig.lockedforrole === true) {\n message = await getString('error_http403blocked', 'local_ai_manager');\n return message;\n }\n if (chatConfig.limitreached === true) {\n message = await getString('error_limitreached', 'local_ai_manager');\n return message;\n }\n return '';\n};\n\n/**\n * Change to openfull view when screen is small.\n * @param {*} e\n */\nconst handleScreenWidthChange = (e) => {\n const body = document.querySelector('body');\n if (e.matches) {\n // Screen width is less than 576px\n body.classList.remove(VIEW_CHATWINDOW, VIEW_OPENFULL, VIEW_DOCKRIGHT);\n body.classList.add(VIEW_OPENFULL);\n } else {\n body.classList.remove(VIEW_CHATWINDOW, VIEW_OPENFULL, VIEW_DOCKRIGHT);\n body.classList.add(viewmode);\n }\n};\n"],"names":["VIEW_CHATWINDOW","VIEW_OPENFULL","VIEW_DOCKRIGHT","strHistory","strNewDialog","strToday","strYesterday","badge","viewmode","modal","modalopen","conversation","id","messages","allConversations","userid","contextid","firstLoad","aiAtWork","maxHistory","maxHistoryWarnings","Set","tenantConfig","chatConfig","DialogModal","Modal","configure","modalConfig","show","removeOnClose","isVerticallyCentered","titletest","setTitletest","value","hide","document","querySelector","classList","remove","async","params","new","history","aiConfig","purposes","find","p","purpose","create","templateContext","title","getRoot","on","e","target","add","ModalEvents","outsideClick","event","checkOutsideClick","setView","getElementById","addEventListener","textarea","addTextareaListener","clickSubmitButton","getConversations","showConversation","conversationcontextLimit","externalServices","getConversationcontextLimit","limit","newDialog","deleteCurrentDialog","showHistory","message","userAllowed","notice","helper","focustextarea","showModal","window","matchMedia","handleScreenWidthChange","innerWidth","getAllConversations","error","x","at","length","clearMessages","setModalHeader","showMessages","enterQuestion","question","showMessage","currentUserLanguage","Config","language","substring","LangNames","Intl","DisplayNames","type","push","of","convHistory","checkMessageHistoryLengthLimit","options","idresult","getNewConversationId","timecreated","Math","floor","Date","now","forcenewitemid","itemid","requestresult","manager","askLocalAiManager","code","errorHandling","copy","copyToClipboard","showReply","result","saveConversationLocally","innerHTML","fields","querySelectorAll","field","text","awaitdivs","forEach","val","sender","answer","templateData","html","js","Templates","renderForPromise","appendNodeContents","attachCopyListenerLast","scrollToBottom","deleted","undefined","then","deleteConversation","removeFromHistory","catch","groupedByDate","convo","date","today","getFullYear","getMonth","getDate","yesterday","twoWeeksAgo","setDate","weekday","day","month","monthOptions","year","dateString","toLocaleDateString","hours","getHours","minutes","getMinutes","toString","padStart","convItem","groups","Object","keys","map","key","objects","filter","obj","reply","hideinput","output","input","style","display","setTitle","modalheader","textareaOnKeydown","height","computedStyles","getComputedStyle","lineHeight","parseFloat","paddingTop","paddingBottom","borderTop","borderTopWidth","borderBottom","borderBottomWidth","maxHeight","newHeight","min","scrollHeight","shiftKey","preventDefault","errorString","JSON","parse","answerdivs","closest","shortenedMessages","slice","has","maxHistoryString","warningErrorString","mode","savedmode","LocalStorage","get","set","body","tenantenabled","userconfirmed","link","location","origin","userlocked","isconfigured","lockedforrole","limitreached","matches"],"mappings":"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"} \ No newline at end of file diff --git a/amd/build/helper.min.js b/amd/build/helper.min.js index acf1467..8f90271 100644 --- a/amd/build/helper.min.js +++ b/amd/build/helper.min.js @@ -1,3 +1,3 @@ -define("block_ai_chat/helper",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.scrollToBottom=_exports.hash=_exports.focustextarea=_exports.escapeHTML=_exports.copyToClipboard=_exports.attachCopyListenerLast=void 0;const copyToClipboard=element=>{const textElement=element.nextElementSibling,textToCopy=textElement.innerText||textElement.textContent;navigator.clipboard.writeText(textToCopy);const toast=element.previousElementSibling;toast.style.visibility="visible",setTimeout((()=>{toast.style.visibility="hidden"}),750)};_exports.copyToClipboard=copyToClipboard;_exports.attachCopyListenerLast=()=>{const elements=document.querySelectorAll(".ai_chat_modal .copy"),last=elements[elements.length-1];last.addEventListener("click",(function(){copyToClipboard(last)}))};_exports.focustextarea=()=>{document.getElementById("block_ai_chat-input-id").focus()};_exports.scrollToBottom=()=>{const modalContent=document.querySelector(".ai_chat_modal .modal-body .block_ai_chat-output-wrapper");modalContent.scrollTop=modalContent.scrollHeight};_exports.escapeHTML=str=>{if(null==str)return"";const escapeMap={"&":"&","<":"<",">":">",'"':""","'":"'","`":"`","/":"/"};return String(str).replace(/[&<>"'`/]/g,(function(match){return escapeMap[match]}))};_exports.hash=async stringToHash=>{const data=(new TextEncoder).encode(stringToHash),hashAsArrayBuffer=await window.crypto.subtle.digest("SHA-256",data),uint8ViewOfHash=new Uint8Array(hashAsArrayBuffer);return Array.from(uint8ViewOfHash).map((b=>b.toString(16).padStart(2,"0"))).join("")}})); +define("block_ai_chat/helper",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.scrollToBottom=_exports.hash=_exports.focustextarea=_exports.escapeHTML=_exports.copyToClipboard=_exports.attachCopyListenerLast=void 0;const copyToClipboard=element=>{const textElement=element.nextElementSibling,textToCopy=textElement.innerText||textElement.textContent;navigator.clipboard.writeText(textToCopy);const toast=element.previousElementSibling;toast.style.visibility="visible",setTimeout((()=>{toast.style.visibility="hidden"}),750)};_exports.copyToClipboard=copyToClipboard;_exports.attachCopyListenerLast=()=>{const elements=document.querySelectorAll(".block_ai_chat_modal .copy"),last=elements[elements.length-1];last.addEventListener("click",(function(){copyToClipboard(last)}))};_exports.focustextarea=()=>{document.getElementById("block_ai_chat-input-id").focus()};_exports.scrollToBottom=()=>{const modalContent=document.querySelector(".block_ai_chat_modal .modal-body .block_ai_chat-output-wrapper");modalContent.scrollTop=modalContent.scrollHeight};_exports.escapeHTML=str=>{if(null==str)return"";const escapeMap={"&":"&","<":"<",">":">",'"':""","'":"'","`":"`","/":"/"};return String(str).replace(/[&<>"'`/]/g,(function(match){return escapeMap[match]}))};_exports.hash=async stringToHash=>{const data=(new TextEncoder).encode(stringToHash),hashAsArrayBuffer=await window.crypto.subtle.digest("SHA-256",data),uint8ViewOfHash=new Uint8Array(hashAsArrayBuffer);return Array.from(uint8ViewOfHash).map((b=>b.toString(16).padStart(2,"0"))).join("")}})); //# sourceMappingURL=helper.min.js.map \ No newline at end of file diff --git a/amd/build/helper.min.js.map b/amd/build/helper.min.js.map index 8bbdfc3..f9fdc6d 100644 --- a/amd/build/helper.min.js.map +++ b/amd/build/helper.min.js.map @@ -1 +1 @@ -{"version":3,"file":"helper.min.js","sources":["../src/helper.js"],"sourcesContent":["/**\n * Copy ai reply to clipboard.\n * @param {*} element\n */\nexport const copyToClipboard = (element) => {\n\n // Find the adjacent text container.\n const textElement = element.nextElementSibling;\n\n // Get the text content.\n const textToCopy = textElement.innerText || textElement.textContent;\n\n // Copy to clipboard using the Clipboard API.\n navigator.clipboard.writeText(textToCopy);\n\n // Briefly show toast.\n const toast = element.previousElementSibling;\n toast.style.visibility = 'visible';\n setTimeout(() => {\n toast.style.visibility = 'hidden';\n }, 750);\n\n};\n\n/**\n * Attach copy listener to all elements.\n */\nexport const attachCopyListenerLast = () => {\n const elements = document.querySelectorAll(\".ai_chat_modal .copy\");\n const last = elements[elements.length - 1];\n last.addEventListener('click', function() {\n copyToClipboard(last);\n });\n};\n\n\n/**\n * Focus textarea.\n */\nexport const focustextarea = () => {\n const textarea = document.getElementById('block_ai_chat-input-id');\n textarea.focus();\n};\n\n\n/**\n * Scroll to bottom of modal body.\n */\nexport const scrollToBottom = () => {\n const modalContent = document.querySelector('.ai_chat_modal .modal-body .block_ai_chat-output-wrapper');\n modalContent.scrollTop = modalContent.scrollHeight;\n};\n\n\n/**\n * Escape html.\n * @param {*} str\n */\nexport const escapeHTML = (str) => {\n if (str === null || str === undefined) {\n return '';\n }\n const escapeMap = {\n '&': '&',\n '<': '<',\n '>': '>',\n '\"': '"',\n \"'\": ''',\n '`': '`',\n '/': '/',\n };\n\n return String(str).replace(/[&<>\"'`/]/g, function(match) {\n return escapeMap[match];\n });\n};\n\n/**\n * Hash function to get a hash of a string.\n *\n * @param {string} stringToHash the string to hash\n * @returns {Promise} the promise containing a hex representation of the string encoded by SHA-256\n */\nexport const hash = async(stringToHash) => {\n const encoder = new TextEncoder();\n const data = encoder.encode(stringToHash);\n const hashAsArrayBuffer = await window.crypto.subtle.digest(\"SHA-256\", data);\n const uint8ViewOfHash = new Uint8Array(hashAsArrayBuffer);\n return Array.from(uint8ViewOfHash)\n .map((b) => b.toString(16).padStart(2, \"0\"))\n .join(\"\");\n};\n"],"names":["copyToClipboard","element","textElement","nextElementSibling","textToCopy","innerText","textContent","navigator","clipboard","writeText","toast","previousElementSibling","style","visibility","setTimeout","elements","document","querySelectorAll","last","length","addEventListener","getElementById","focus","modalContent","querySelector","scrollTop","scrollHeight","str","escapeMap","String","replace","match","async","data","TextEncoder","encode","stringToHash","hashAsArrayBuffer","window","crypto","subtle","digest","uint8ViewOfHash","Uint8Array","Array","from","map","b","toString","padStart","join"],"mappings":"6QAIaA,gBAAmBC,gBAGtBC,YAAcD,QAAQE,mBAGtBC,WAAaF,YAAYG,WAAaH,YAAYI,YAGxDC,UAAUC,UAAUC,UAAUL,kBAGxBM,MAAQT,QAAQU,uBACtBD,MAAME,MAAMC,WAAa,UACzBC,YAAW,KACRJ,MAAME,MAAMC,WAAa,WACzB,+EAO+B,WAC5BE,SAAWC,SAASC,iBAAiB,wBACrCC,KAAOH,SAASA,SAASI,OAAS,GACxCD,KAAKE,iBAAiB,SAAS,WAC3BpB,gBAAgBkB,iCAQK,KACRF,SAASK,eAAe,0BAChCC,iCAOiB,WACpBC,aAAeP,SAASQ,cAAc,4DAC5CD,aAAaE,UAAYF,aAAaG,kCAQfC,SACnBA,MAAAA,UACO,SAELC,UAAY,KACT,YACA,WACA,WACA,aACA,YACA,aACA,iBAGFC,OAAOF,KAAKG,QAAQ,cAAc,SAASC,cACvCH,UAAUG,yBAULC,MAAAA,qBAEVC,MADU,IAAIC,aACCC,OAAOC,cACtBC,wBAA0BC,OAAOC,OAAOC,OAAOC,OAAO,UAAWR,MACjES,gBAAkB,IAAIC,WAAWN,0BAChCO,MAAMC,KAAKH,iBACbI,KAAKC,GAAMA,EAAEC,SAAS,IAAIC,SAAS,EAAG,OACtCC,KAAK"} \ No newline at end of file +{"version":3,"file":"helper.min.js","sources":["../src/helper.js"],"sourcesContent":["/**\n * Copy ai reply to clipboard.\n * @param {*} element\n */\nexport const copyToClipboard = (element) => {\n\n // Find the adjacent text container.\n const textElement = element.nextElementSibling;\n\n // Get the text content.\n const textToCopy = textElement.innerText || textElement.textContent;\n\n // Copy to clipboard using the Clipboard API.\n navigator.clipboard.writeText(textToCopy);\n\n // Briefly show toast.\n const toast = element.previousElementSibling;\n toast.style.visibility = 'visible';\n setTimeout(() => {\n toast.style.visibility = 'hidden';\n }, 750);\n\n};\n\n/**\n * Attach copy listener to all elements.\n */\nexport const attachCopyListenerLast = () => {\n const elements = document.querySelectorAll(\".block_ai_chat_modal .copy\");\n const last = elements[elements.length - 1];\n last.addEventListener('click', function() {\n copyToClipboard(last);\n });\n};\n\n\n/**\n * Focus textarea.\n */\nexport const focustextarea = () => {\n const textarea = document.getElementById('block_ai_chat-input-id');\n textarea.focus();\n};\n\n\n/**\n * Scroll to bottom of modal body.\n */\nexport const scrollToBottom = () => {\n const modalContent = document.querySelector('.block_ai_chat_modal .modal-body .block_ai_chat-output-wrapper');\n modalContent.scrollTop = modalContent.scrollHeight;\n};\n\n\n/**\n * Escape html.\n * @param {*} str\n */\nexport const escapeHTML = (str) => {\n if (str === null || str === undefined) {\n return '';\n }\n const escapeMap = {\n '&': '&',\n '<': '<',\n '>': '>',\n '\"': '"',\n \"'\": ''',\n '`': '`',\n '/': '/',\n };\n\n return String(str).replace(/[&<>\"'`/]/g, function(match) {\n return escapeMap[match];\n });\n};\n\n/**\n * Hash function to get a hash of a string.\n *\n * @param {string} stringToHash the string to hash\n * @returns {Promise} the promise containing a hex representation of the string encoded by SHA-256\n */\nexport const hash = async(stringToHash) => {\n const encoder = new TextEncoder();\n const data = encoder.encode(stringToHash);\n const hashAsArrayBuffer = await window.crypto.subtle.digest(\"SHA-256\", data);\n const uint8ViewOfHash = new Uint8Array(hashAsArrayBuffer);\n return Array.from(uint8ViewOfHash)\n .map((b) => b.toString(16).padStart(2, \"0\"))\n .join(\"\");\n};\n"],"names":["copyToClipboard","element","textElement","nextElementSibling","textToCopy","innerText","textContent","navigator","clipboard","writeText","toast","previousElementSibling","style","visibility","setTimeout","elements","document","querySelectorAll","last","length","addEventListener","getElementById","focus","modalContent","querySelector","scrollTop","scrollHeight","str","escapeMap","String","replace","match","async","data","TextEncoder","encode","stringToHash","hashAsArrayBuffer","window","crypto","subtle","digest","uint8ViewOfHash","Uint8Array","Array","from","map","b","toString","padStart","join"],"mappings":"6QAIaA,gBAAmBC,gBAGtBC,YAAcD,QAAQE,mBAGtBC,WAAaF,YAAYG,WAAaH,YAAYI,YAGxDC,UAAUC,UAAUC,UAAUL,kBAGxBM,MAAQT,QAAQU,uBACtBD,MAAME,MAAMC,WAAa,UACzBC,YAAW,KACRJ,MAAME,MAAMC,WAAa,WACzB,+EAO+B,WAC5BE,SAAWC,SAASC,iBAAiB,8BACrCC,KAAOH,SAASA,SAASI,OAAS,GACxCD,KAAKE,iBAAiB,SAAS,WAC3BpB,gBAAgBkB,iCAQK,KACRF,SAASK,eAAe,0BAChCC,iCAOiB,WACpBC,aAAeP,SAASQ,cAAc,kEAC5CD,aAAaE,UAAYF,aAAaG,kCAQfC,SACnBA,MAAAA,UACO,SAELC,UAAY,KACT,YACA,WACA,WACA,aACA,YACA,aACA,iBAGFC,OAAOF,KAAKG,QAAQ,cAAc,SAASC,cACvCH,UAAUG,yBAULC,MAAAA,qBAEVC,MADU,IAAIC,aACCC,OAAOC,cACtBC,wBAA0BC,OAAOC,OAAOC,OAAOC,OAAO,UAAWR,MACjES,gBAAkB,IAAIC,WAAWN,0BAChCO,MAAMC,KAAKH,iBACbI,KAAKC,GAAMA,EAAEC,SAAS,IAAIC,SAAS,EAAG,OACtCC,KAAK"} \ No newline at end of file diff --git a/amd/src/dialog.js b/amd/src/dialog.js index f9fe907..2e2136a 100644 --- a/amd/src/dialog.js +++ b/amd/src/dialog.js @@ -111,7 +111,7 @@ export const init = async(params) => { // Add class for styling when modal is displayed. modal.getRoot().on('modal:shown', function(e) { - e.target.classList.add("ai_chat_modal"); + e.target.classList.add("block_ai_chat_modal"); }); // Conditionally prevent outside click event. @@ -211,7 +211,9 @@ async function showModal() { // Show userquota. await renderUserQuota('#block_ai_chat_userquota', ['chat']); // Show infobox. - await renderInfoBox('block_ai_chat', userid, '.ai_chat_modal_body [data-content="local_ai_manager_infobox"]', ['chat']); + await renderInfoBox( + 'block_ai_chat', userid, '.block_ai_chat_modal_body [data-content="local_ai_manager_infobox"]', ['chat'] + ); // Check if all permissions and settings are correct. const message = await userAllowed(); @@ -332,7 +334,7 @@ const enterQuestion = async(question) => { } // Attach copy listener. - let copy = document.querySelector('.ai_chat_modal .awaitanswer .copy'); + let copy = document.querySelector('.block_ai_chat_modal .awaitanswer .copy'); copy.addEventListener('mousedown', () => { helper.copyToClipboard(copy); }); @@ -358,14 +360,14 @@ const enterQuestion = async(question) => { */ const showReply = async(text) => { // Get textblock. - let fields = document.querySelectorAll('.ai_chat_modal .awaitanswer .text'); + let fields = document.querySelectorAll('.block_ai_chat_modal .awaitanswer .text'); const field = fields[fields.length - 1]; // Render the reply. field.innerHTML = text; field.classList.remove('small'); // Remove awaitanswer class. - let awaitdivs = document.querySelectorAll('.ai_chat_modal .awaitanswer'); + let awaitdivs = document.querySelectorAll('.block_ai_chat_modal .awaitanswer'); const awaitdiv = awaitdivs[awaitdivs.length - 1]; awaitdiv.classList.remove('awaitanswer'); }; @@ -485,7 +487,7 @@ const showHistory = async() => { }); // Set modal class to hide info about ratelimits and infobox. - let modal = document.querySelector('.ai_chat_modal'); + let modal = document.querySelector('.block_ai_chat_modal'); modal.classList.add('onhistorypage'); // Iterate over conversations and group by date. @@ -549,7 +551,7 @@ const showHistory = async() => { "dates": convert.groups, }; const {html, js} = await Templates.renderForPromise('block_ai_chat/history', templateData); - Templates.appendNodeContents('.ai_chat_modal .block_ai_chat-output', html, js); + Templates.appendNodeContents('.block_ai_chat_modal .block_ai_chat-output', html, js); // Add a listener for the new dialog button. const btnNewDialog = document.getElementById('ai_chat_history_new_dialog'); @@ -603,7 +605,7 @@ const clearMessages = (hideinput = false) => { * @param {*} setTitle */ const setModalHeader = (setTitle = '') => { - let modalheader = document.querySelector('.ai_chat_modal .modal-title div'); + let modalheader = document.querySelector('.block_ai_chat_modal .modal-title div'); let title = ''; if (modalheader !== null && (conversation.messages.length > 0 || setTitle.length)) { if (!setTitle.length) { @@ -614,7 +616,7 @@ const setModalHeader = (setTitle = '') => { modalheader.innerHTML = title; } // Remove onhistorypage, since history page is setting it. - let modal = document.querySelector('.ai_chat_modal'); + let modal = document.querySelector('.block_ai_chat_modal'); modal.classList.remove('onhistorypage'); }; diff --git a/amd/src/helper.js b/amd/src/helper.js index d82e1eb..e4c0d30 100644 --- a/amd/src/helper.js +++ b/amd/src/helper.js @@ -26,7 +26,7 @@ export const copyToClipboard = (element) => { * Attach copy listener to all elements. */ export const attachCopyListenerLast = () => { - const elements = document.querySelectorAll(".ai_chat_modal .copy"); + const elements = document.querySelectorAll(".block_ai_chat_modal .copy"); const last = elements[elements.length - 1]; last.addEventListener('click', function() { copyToClipboard(last); @@ -47,7 +47,7 @@ export const focustextarea = () => { * Scroll to bottom of modal body. */ export const scrollToBottom = () => { - const modalContent = document.querySelector('.ai_chat_modal .modal-body .block_ai_chat-output-wrapper'); + const modalContent = document.querySelector('.block_ai_chat_modal .modal-body .block_ai_chat-output-wrapper'); modalContent.scrollTop = modalContent.scrollHeight; }; diff --git a/lang/de/block_ai_chat.php b/lang/de/block_ai_chat.php index ca7d54c..e05dc45 100644 --- a/lang/de/block_ai_chat.php +++ b/lang/de/block_ai_chat.php @@ -53,7 +53,7 @@ $string['noticenewquestion'] = 'Aktuell können sie keine Frage mehr übermitteln'; $string['openfull'] = 'Vollflächig öffnen'; $string['pluginname'] = 'KI-Chat'; -$string['privacy:metadata'] = 'Dieses Plugin speichert keine Daten, Konversationen werden im AI Manager gespeichert.'; +$string['privacy:metadata'] = 'Konversationen werden im AI Manager gespeichert.'; $string['private'] = 'Privat'; $string['public'] = 'Öffentlich'; $string['replacehelp'] = 'Ai Chat-Button anstelle des Hilfe-Buttons anzeigen.'; diff --git a/lang/en/block_ai_chat.php b/lang/en/block_ai_chat.php index 4882f1d..99c9d36 100644 --- a/lang/en/block_ai_chat.php +++ b/lang/en/block_ai_chat.php @@ -53,7 +53,7 @@ $string['noticenewquestion'] = 'Currently you cant submit a new question'; $string['openfull'] = 'Use full width'; $string['pluginname'] = 'AI Chat'; -$string['privacy:metadata'] = 'This plugin doesn\'t store personal data.'; +$string['privacy:metadata'] = 'Conversations are saved by local_ai_manager.'; $string['private'] = 'Private'; $string['public'] = 'Public'; $string['replacehelp'] = 'Replace helpbutton with block_ai_chat button'; diff --git a/styles.css b/styles.css index 0504a0b..3c8af3b 100644 --- a/styles.css +++ b/styles.css @@ -27,85 +27,85 @@ right: calc(315px + 2rem); } -.ai_chat_modal .modal-dialog { +.block_ai_chat_modal .modal-dialog { min-height: 500px; } -.ai_chat_modal .modal-header { +.block_ai_chat_modal .modal-header { width: 100%; border-bottom: none; } -.ai_chat_modal .modal-header .modal-title { +.block_ai_chat_modal .modal-header .modal-title { display: flex; justify-content: space-between; align-items: center; width: calc(100% - 2rem); font-size: 1rem; } -.ai_chat_modal .modal-header .modal-title .ai_chat_title { +.block_ai_chat_modal .modal-header .modal-title .block_ai_chat_title { text-overflow: ellipsis; white-space: nowrap; overflow: hidden; } -.ai_chat_modal .modal-body { +.block_ai_chat_modal .modal-body { display: flex; padding: 0 0 0 1rem; } -.ai_chat_modal .modal-body .block_ai_chat-dialog { +.block_ai_chat_modal .modal-body .block_ai_chat-dialog { display: flex; flex-direction: column; justify-content: space-between; width: 100%; height: 100%; } -.ai_chat_modal .content { +.block_ai_chat_modal .content { display: flex; align-items: flex-start; padding: 0.5rem 1rem 0.5rem 1rem; position: relative; } -.ai_chat_modal .message .copy { +.block_ai_chat_modal .message .copy { visibility: hidden; position: absolute; top: 0; right: 0; padding: 0.5rem 0.75rem 2.5rem 2.75rem; } -.ai_chat_modal .message.ai:hover .copy { +.block_ai_chat_modal .message.ai:hover .copy { visibility: visible; cursor: pointer; } -.ai_chat_modal .message.ai .content { +.block_ai_chat_modal .message.ai .content { padding-left: 0; } -.ai_chat_modal .message.ai .content .text { +.block_ai_chat_modal .message.ai .content .text { margin-top: 0.4rem; } -.ai_chat_modal .message.ai .content .text p { +.block_ai_chat_modal .message.ai .content .text p { margin-bottom: 0.3rem; } -.ai_chat_modal .message.ai .content .ai_chat_icon { +.block_ai_chat_modal .message.ai .content .ai_chat_icon { margin-top: 0.8rem; } -.ai_chat_modal .message.ai .content .spinner-border { +.block_ai_chat_modal .message.ai .content .spinner-border { width: 1.5rem; height: 1.5rem; } -.ai_chat_modal .message.ai .content.awaitanswer .ai_chat_icon, -.ai_chat_modal .message.ai .content.awaitanswer .copy { +.block_ai_chat_modal .message.ai .content.awaitanswer .ai_chat_icon, +.block_ai_chat_modal .message.ai .content.awaitanswer .copy { display: none; } -.ai_chat_modal .message.agent .content { +.block_ai_chat_modal .message.agent .content { background-color: #dee2e6; margin-left: 4rem; border-radius: 0.5rem; } -.ai_chat_modal .message.agent .content .ai_chat_icon { +.block_ai_chat_modal .message.agent .content .ai_chat_icon { display: none; } -.ai_chat_modal .message.agent .content p { +.block_ai_chat_modal .message.agent .content p { margin-bottom: 0.1rem; } -.ai_chat_modal .copiedtoast { +.block_ai_chat_modal .copiedtoast { position: absolute; top: -15px; right: 0; @@ -115,24 +115,24 @@ background-color: #fff; visibility: hidden; } -.ai_chat_modal .block_ai_chat-input-wrapper { +.block_ai_chat_modal .block_ai_chat-input-wrapper { position: sticky; bottom: 0; background-color: #fff; padding-right: 1rem; } -.ai_chat_modal .block_ai_chat-input { +.block_ai_chat_modal .block_ai_chat-input { display: flex; margin-top: 0.15rem; padding: 0 0.1rem 0 0.1rem; position: relative; } -.ai_chat_modal .block_ai_chat-input .ai_chat_icon { +.block_ai_chat_modal .block_ai_chat-input .ai_chat_icon { position: absolute; top: 1.2rem; left: 0.5rem; } -.ai_chat_modal .block_ai_chat-input textarea { +.block_ai_chat_modal .block_ai_chat-input textarea { height: inherit; padding: 1rem 4.1rem 1rem 2.2rem; resize: none; @@ -143,44 +143,44 @@ box-sizing: border-box; margin-left: 2px; } -.ai_chat_modal .block_ai_chat-input textarea::placeholder { +.block_ai_chat_modal .block_ai_chat-input textarea::placeholder { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.ai_chat_modal .block_ai_chat-input button { +.block_ai_chat_modal .block_ai_chat-input button { position: absolute; display: flex; bottom: 0.5rem; right: 0.7rem; } -.ai_chat_modal .block_ai_chat-input button i { +.block_ai_chat_modal .block_ai_chat-input button i { padding: 6.5px 0; } -.ai_chat_modal .headeroptions { +.block_ai_chat_modal .headeroptions { display: flex; align-items: center; } -.ai_chat_modal .headeroptions .badge-pill { +.block_ai_chat_modal .headeroptions .badge-pill { display: flex; align-items: center; height: 1.3rem; font-size: 50%; } -.ai_chat_modal .headeroptions .dropdown-menu { +.block_ai_chat_modal .headeroptions .dropdown-menu { left: 36px !important; } -.ai_chat_modal .headeroptions .dropdown-menu #block_ai_chat_delete_dialog { +.block_ai_chat_modal .headeroptions .dropdown-menu #block_ai_chat_delete_dialog { color: #ff3333; } -.ai_chat_modal .headeroptions .dropdown-menu #block_ai_chat_delete_dialog:hover { +.block_ai_chat_modal .headeroptions .dropdown-menu #block_ai_chat_delete_dialog:hover { background-color: #ffcccc; } -.ai_chat_modal #block_ai_chat_userquota { +.block_ai_chat_modal #block_ai_chat_userquota { text-align: right; min-height: 1rem; } -.ai_chat_modal #block_ai_chat_userquota .local_ai_manager_userquota_infobox { +.block_ai_chat_modal #block_ai_chat_userquota .local_ai_manager_userquota_infobox { color: #6c757d; background-color: inherit; font-size: 0.6rem; @@ -188,7 +188,7 @@ margin: 0; text-align: right; } -.ai_chat_modal .gradient-overlay { +.block_ai_chat_modal .gradient-overlay { content: ""; position: sticky; top: 0; @@ -198,11 +198,11 @@ background: linear-gradient(to bottom, #fff 10%, transparent); z-index: 33; } -.ai_chat_modal .ai_chat_modal_body { +.block_ai_chat_modal .block_ai_chat_modal_body { width: 100%; overflow: hidden; } -.ai_chat_modal .ai_chat_modal_body .infobox { +.block_ai_chat_modal .block_ai_chat_modal_body .infobox { display: flex; align-items: center; flex-direction: row-reverse; @@ -211,64 +211,58 @@ z-index: 1; min-height: 2rem; } -.ai_chat_modal .ai_chat_modal_body .infobox .local_ai_manager-infobox.alert.alert-success { +.block_ai_chat_modal .block_ai_chat_modal_body .infobox .local_ai_manager-infobox.alert.alert-success { color: #6c757d; background-color: inherit; font-size: 0.6rem; padding: 0; margin: 0; } -.ai_chat_modal .block_ai_chat-output-wrapper { +.block_ai_chat_modal .block_ai_chat-output-wrapper { overflow-y: auto; height: 100%; padding-right: 1rem; } -.ai_chat_modal .block_ai-history-items { +.block_ai_chat_modal .block_ai-history-items { padding: 0.5rem; } -.ai_chat_modal .block_ai-history-items a { +.block_ai_chat_modal .block_ai-history-items a { display: flex; align-items: baseline; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; } -.ai_chat_modal .block_ai-history-items a p { +.block_ai_chat_modal .block_ai-history-items a p { margin: 0; } -.ai_chat_modal .block_ai-history-items > div { +.block_ai_chat_modal .block_ai-history-items > div { display: flex; flex-wrap: nowrap; justify-content: space-between; gap: 0.2rem; } -.ai_chat_modal .block_ai-history-items.card { +.block_ai_chat_modal .block_ai-history-items.card { flex-direction: column-reverse; } -.ai_chat_modal.onhistorypage .block_ai_chat-output { +.block_ai_chat_modal.onhistorypage .block_ai_chat-output { display: flex; flex-direction: column-reverse; } -.ai_chat_modal.onhistorypage .infobox, -.ai_chat_modal.onhistorypage #block_ai_chat_userquota { +.block_ai_chat_modal.onhistorypage .infobox, +.block_ai_chat_modal.onhistorypage #block_ai_chat_userquota { display: none; } -.ai_chat_modal.onhistorypage .gradient-overlay { +.block_ai_chat_modal.onhistorypage .gradient-overlay { display: none; } @media (min-width: 497px) { - .ai_chat_modal.onhistorypage .modal-body { + .block_ai_chat_modal.onhistorypage .modal-body { min-width: 497px; } } -.ai_fake_block { - visibility: hidden; - height: 0; - margin: 0; -} - -body.block_ai_chat_chatwindow .ai_chat_modal .modal-dialog { +body.block_ai_chat_chatwindow .block_ai_chat_modal .modal-dialog { position: fixed; max-height: calc(100vh - 6rem); right: 2rem; @@ -284,18 +278,18 @@ body.block_ai_chat_dockright { overflow: initial; } body.block_ai_chat_chatwindow .modal-backdrop.show, -body.block_ai_chat_chatwindow .ai_chat_modal, +body.block_ai_chat_chatwindow .block_ai_chat_modal, body.block_ai_chat_dockright .modal-backdrop.show, -body.block_ai_chat_dockright .ai_chat_modal { +body.block_ai_chat_dockright .block_ai_chat_modal { width: 0; height: 0; } -body.block_ai_chat_openfull .ai_chat_modal .modal-dialog { +body.block_ai_chat_openfull .block_ai_chat_modal .modal-dialog { max-width: 95%; min-height: calc(100vh - 3.5rem); } -body.block_ai_chat_openfull .ai_chat_modal .ai_chat_modal_body { +body.block_ai_chat_openfull .block_ai_chat_modal .block_ai_chat_modal_body { max-width: 1080px; margin: 0 auto; } @@ -306,7 +300,7 @@ body.block_ai_chat_openfull a#block_ai_chat_openfull { body.block_ai_chat_dockright.block_ai_chat_open #page-wrapper #page { margin-right: 50%; } -body.block_ai_chat_dockright .ai_chat_modal .modal-dialog { +body.block_ai_chat_dockright .block_ai_chat_modal .modal-dialog { position: fixed; width: 50%; max-width: 50%; @@ -316,7 +310,7 @@ body.block_ai_chat_dockright .ai_chat_modal .modal-dialog { margin-top: 0; margin-bottom: 0; } -body.block_ai_chat_dockright .ai_chat_modal .modal-dialog .modal-content { +body.block_ai_chat_dockright .block_ai_chat_modal .modal-dialog .modal-content { border-radius: 0; border-top: none; border-bottom: none; diff --git a/styles.scss b/styles.scss index fcb4dc0..4fcc533 100644 --- a/styles.scss +++ b/styles.scss @@ -31,7 +31,7 @@ } -.ai_chat_modal{ +.block_ai_chat_modal{ .modal-dialog { min-height: 500px; } @@ -44,7 +44,7 @@ align-items: center; width: calc(100% - 2rem); font-size: 1rem; - .ai_chat_title { + .block_ai_chat_title { text-overflow: ellipsis; white-space: nowrap; overflow: hidden; @@ -209,7 +209,7 @@ background: linear-gradient(to bottom, #fff 10%, transparent); z-index: 33; } - .ai_chat_modal_body { + .block_ai_chat_modal_body { width: 100%; overflow: hidden; .infobox { @@ -271,21 +271,15 @@ } } @media (min-width: 497px) { - .ai_chat_modal.onhistorypage { + .block_ai_chat_modal.onhistorypage { .modal-body { min-width: 497px; } } } -.ai_fake_block { - visibility: hidden; - height: 0; - margin: 0; -} - body.block_ai_chat_chatwindow { - .ai_chat_modal .modal-dialog { + .block_ai_chat_modal .modal-dialog { position: fixed; max-height: calc(100vh - 6rem); right: 2rem; @@ -300,19 +294,19 @@ body.block_ai_chat_chatwindow, body.block_ai_chat_dockright { overflow: initial; .modal-backdrop.show, - .ai_chat_modal { + .block_ai_chat_modal { width: 0; height: 0; } } body.block_ai_chat_openfull { - .ai_chat_modal { + .block_ai_chat_modal { .modal-dialog { max-width: 95%; min-height: calc(100vh - 3.5rem); } - .ai_chat_modal_body { + .block_ai_chat_modal_body { max-width: 1080px; margin: 0 auto; } @@ -325,7 +319,7 @@ body.block_ai_chat_dockright { &.block_ai_chat_open #page-wrapper #page { margin-right: 50%; } - .ai_chat_modal .modal-dialog { + .block_ai_chat_modal .modal-dialog { position: fixed; width: 50%; max-width: 50%; diff --git a/templates/dialog_modal.mustache b/templates/dialog_modal.mustache index cc7b5b9..ed01c56 100644 --- a/templates/dialog_modal.mustache +++ b/templates/dialog_modal.mustache @@ -40,11 +40,11 @@ along with Moodle. If not, see . {{< core/modal }} {{$title}} -
{{title}}
+
{{title}}
{{> block_ai_chat/headeroptions }} {{/title}} {{$body}} -
+