From 65604c0ae4da481cca3341fae363332204b4caff Mon Sep 17 00:00:00 2001 From: Hannes Funk Date: Sun, 19 Jul 2026 23:00:00 +0200 Subject: [PATCH] MBS-10501: Grey out chat when availability hideen --- amd/build/components/chat.min.js | 2 +- amd/build/components/chat.min.js.map | 2 +- amd/src/components/chat.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/amd/build/components/chat.min.js b/amd/build/components/chat.min.js index 94206dc..5a955c2 100644 --- a/amd/build/components/chat.min.js +++ b/amd/build/components/chat.min.js @@ -1,3 +1,3 @@ -define("block_ai_chat/components/chat",["exports","block_ai_chat/components/base_content","tiny_ai/utils","tiny_ai/editor_utils","tiny_ai/constants","core/templates","local_ai_manager/config","core/str","core/log","core/notification","block_ai_chat/utils","block_ai_chat/constants","block_ai_chat/dom_extractor","core/utils"],(function(_exports,_base_content,TinyAiUtils,_editor_utils,_constants,_templates,_config,_str,_log,_notification,_utils2,_constants2,DomExtractor,_utils3){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_base_content=_interopRequireDefault(_base_content),TinyAiUtils=_interopRequireWildcard(TinyAiUtils),_editor_utils=_interopRequireDefault(_editor_utils),_templates=_interopRequireDefault(_templates),_log=_interopRequireDefault(_log),DomExtractor=_interopRequireWildcard(DomExtractor);var _systemImportTransformerGlobalIdentifier="undefined"!=typeof window?window:"undefined"!=typeof self?self:"undefined"!=typeof global?global:{};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}}class Chat extends _base_content.default{static init(target){return new this({element:document.querySelector(target)})}create(){this.name="ChatComponent",this.selectors={MESSAGES:"[data-block_ai_chat-element='messages']",INPUT_TEXTAREA:"[data-block_ai_chat-element='inputtextarea']",SUBMIT_BUTTON:"[data-block_ai_chat-element='submitbutton']",LOADING_SPINNER_MESSAGE:"[data-block_ai_chat-element='loadingspinner']",TEMPORARY_PROMPT_MESSAGE:"[data-block_ai_chat-element='temporaryprompt']",TINY_AI_BUTTON:"[data-block_ai_chat-element='tinyaibutton']",OUTPUT_WRAPPER:"[data-block_ai_chat-element='outputwrapper']",CHAT_OUTPUT:"[data-block_ai_chat-element='chatoutput']",HISTORY_MARKER:"[data-block_ai_chat-element='historymarker']"},this._debouncedScrollToBottom=(0,_utils3.debounce)((messageElement=>this._scrollToBottom(messageElement)),350),this._debouncedFocusInputTextarea=(0,_utils3.debounce)((()=>this._focusInputTextarea()),350)}async stateReady(){this.reactive.dispatch("setView","chat")}getWatchers(){return[...super.getWatchers(),{watch:"messages:created",handler:this._addMessageToChatArea},{watch:"personas".concat(this.reactive.state.config.currentPersona,":deleted"),handler:this._removeCurrentPersona},{watch:"config.loadingState:updated",handler:this._handleLoadingStateUpdated}]}async _addMessageToChatArea(_ref){let{element:element}=_ref,placeholder=document.createElement("div");placeholder.setAttribute("data-id",element.id);let node=this.getElement(this.selectors.CHAT_OUTPUT);node.appendChild(placeholder);const responseIsAgentResponse="agent"===element.messageMode;let templateData={id:element.id,agentMode:responseIsAgentResponse,senderai:"ai"===element.sender,loading:!!element.hasOwnProperty("loading")&&element.loading};if(responseIsAgentResponse){const agentResponse=responseIsAgentResponse?this._getAgentAnswerTemplateContext(element.content):{};templateData={...templateData,...agentResponse}}else templateData.content=element.content;const newelement=(await this.renderComponent(placeholder,"block_ai_chat/components/message",templateData)).getElement();node.replaceChild(newelement,placeholder),this.reactive.dispatch("setMessageRendered",element.id,!0),this._highlightCodeBlocks(newelement),this._debouncedScrollToBottom(newelement),this._debouncedFocusInputTextarea()}async _highlightCodeBlocks(messageElement){if(messageElement.querySelector("pre code"))try{const PrismJS=await("function"==typeof _systemImportTransformerGlobalIdentifier.define&&_systemImportTransformerGlobalIdentifier.define.amd?new Promise((function(resolve,reject){_systemImportTransformerGlobalIdentifier.require(["filter_codehighlighter/prism"],resolve,reject)})):"undefined"!=typeof module&&module.exports&&"undefined"!=typeof require||"undefined"!=typeof module&&module.component&&_systemImportTransformerGlobalIdentifier.require&&"component"===_systemImportTransformerGlobalIdentifier.require.loader?Promise.resolve(require("filter_codehighlighter/prism")):Promise.resolve(_systemImportTransformerGlobalIdentifier["filter_codehighlighter/prism"]));PrismJS.plugins.customClass.prefix("prism-"),PrismJS.highlightAllUnder(messageElement)}catch(exception){_log.default.debug("block_ai_chat: code highlighting unavailable",exception)}}async _submitAiRequestListener(){const prompt=this.getElement(this.selectors.INPUT_TEXTAREA).value;if(""===prompt.trim()){const errorString=(0,_str.getString)("erroremptyprompt","block_ai_chat");return void await(0,_utils2.showErrorToast)(errorString)}const additionalOptions={};this.reactive.state.config.mode===_constants2.MODES.AGENT&&(additionalOptions.agentoptions={formelements:DomExtractor.extractDomElements(),pageid:document.body.id}),this.reactive.dispatch("submitAiRequest",prompt,additionalOptions)}async _handleLoadingStateUpdated(_ref2){let{element:element}=_ref2;const loadingSpinnerMessage={id:"loadingspinner",sender:"ai",loading:!0,agentMode:!1},temporaryPromptMessage={id:"temporaryprompt",sender:"user",content:this.formatPlainText(this.getElement(this.selectors.INPUT_TEXTAREA).value),agentMode:!1};if(element.loadingState){await this._addMessageToChatArea({element:temporaryPromptMessage}),await this._addMessageToChatArea({element:loadingSpinnerMessage});const inputTextarea=this.getElement(this.selectors.INPUT_TEXTAREA);inputTextarea.value="",inputTextarea.dispatchEvent(new Event("input"))}}formatPlainText(text){return text.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/ {2}/g,"  ").replace(/\n/g,"
")}_handleKeyDownOnInputTextarea(event){"Enter"!==event.key||event.shiftKey||(event.preventDefault(),this._submitAiRequestListener())}_removeCurrentPersona(){this.reactive.dispatch("selectCurrentPersona",0)}async _renderContent(){const{html:html,js:js}=await _templates.default.renderForPromise("block_ai_chat/chat_content",{conversationContextLimit:this.reactive.state.config.conversationContextLimit});_templates.default.replaceNodeContents(this.getElement(),html,js),await this._setupAfterContentRendering();const availabilityErrorMessage=await this.isAiChatAvailable();if(""!==availabilityErrorMessage){const notice=await(0,_str.getString)("notice","block_ai_chat");await(0,_notification.alert)(notice,availabilityErrorMessage),this.setElementLocked(this.getElement(this.selectors.INPUT_TEXTAREA),!0),this.setElementLocked(this.getElement(this.selectors.SUBMIT_BUTTON),!0),this.getElement(this.selectors.INPUT_TEXTAREA).disabled=!0,this.getElement(this.selectors.SUBMIT_BUTTON).disabled=!0}}async _setupAfterContentRendering(){this.reactive.dispatch("loadCurrentConversationMessages");const inputTextarea=this.getElement(this.selectors.INPUT_TEXTAREA),sendRequestButton=this.getElement(this.selectors.SUBMIT_BUTTON),tinyAiButton=this.getElement(this.selectors.TINY_AI_BUTTON),uniqid=Math.random().toString(16).slice(2);await TinyAiUtils.init(uniqid,this.reactive.state.static.contextid,_constants.constants.modalModes.standalone),this.addEventListener(tinyAiButton,"click",(async()=>{const selectionObject=window.getSelection();if(selectionObject.rangeCount>0){const range=selectionObject.getRangeAt(0),container=document.createElement("div");container.appendChild(range.cloneContents());const images=container.querySelectorAll("img");if(images.length>0&&images[0].src){const image=images[0],fetchResult=await fetch(image.src),data=await fetchResult.blob();TinyAiUtils.getDatamanager(uniqid).setSelectionImg(data)}selectionObject.toString()&&selectionObject.toString().length>0&&TinyAiUtils.getDatamanager(uniqid).setSelection(selectionObject.toString())}const editorUtils=new _editor_utils.default(uniqid,"block_ai_chat",this.reactive.state.static.contextid,this.reactive.state.static.userid,null);TinyAiUtils.setEditorUtils(uniqid,editorUtils),await editorUtils.displayDialogue()})),this.addEventListener(sendRequestButton,"click",this._submitAiRequestListener),this.addEventListener(inputTextarea,"keydown",this._handleKeyDownOnInputTextarea),this._enableTextAreaAutoResize(),this._debouncedScrollToBottom(),this._debouncedFocusInputTextarea()}getViewName(){return"chat"}_scrollToBottom(messageElement){const chatOutputWrapper=this.getElement(this.selectors.OUTPUT_WRAPPER);if(!messageElement||"message"!==messageElement.dataset.block_ai_chatComponent)return void(chatOutputWrapper.scrollTop=chatOutputWrapper.scrollHeight);const containerHeight=chatOutputWrapper.clientHeight;if(messageElement.offsetHeight>containerHeight){const previousMessage=messageElement.previousElementSibling;if(previousMessage){const promptVisibleHeight=(parseFloat(window.getComputedStyle(previousMessage).lineHeight)||24)*2,scrollPosition=messageElement.offsetTop-promptVisibleHeight;chatOutputWrapper.scrollTop=Math.max(0,scrollPosition)}else chatOutputWrapper.scrollTop=messageElement.offsetTop}else chatOutputWrapper.scrollTop=chatOutputWrapper.scrollHeight}_focusInputTextarea(){const inputTextarea=this.getElement(this.selectors.INPUT_TEXTAREA);requestAnimationFrame((()=>{inputTextarea.focus()}))}_enableTextAreaAutoResize(){const inputTextarea=this.getElement(this.selectors.INPUT_TEXTAREA);this.addEventListener(inputTextarea,"input",(()=>{inputTextarea.style.height="auto";const computedStyles=window.getComputedStyle(inputTextarea),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(inputTextarea.scrollHeight+borderTop+borderBottom,maxHeight);inputTextarea.style.height=newHeight+"px"}))}async isAiChatAvailable(){const contextid=this.reactive.state.static.contextid,aiConfig=await(0,_config.getAiConfig)(contextid,null,["chat"]);return"disabled"===aiConfig.availability.available?aiConfig.availability.errormessage:"disabled"===aiConfig.purposes[0].available?aiConfig.purposes[0].errormessage:""}_getAgentAnswerTemplateContext(content){const agentAnswer=JSON.parse(content),chatOutputIntroObject=agentAnswer.chatoutput.filter((object=>"intro"===object.type))[0],chatOutputOutroObject=agentAnswer.chatoutput.filter((object=>"outro"===object.type))[0],suggestionContext={intro:chatOutputIntroObject.text,suggestions:[],outro:chatOutputOutroObject.text};return agentAnswer.formelements.forEach((formElement=>{const htmlElement=document.getElementById(formElement.id);let newValue=formElement.newValue?formElement.newValue.toString().trim():"";0===newValue.length&&(newValue=0);const displayValue=formElement.suggestiondisplayvalue||"",plainText=displayValue.replace(/<[^>]*>/g,""),isLong=plainText.length>80;suggestionContext.suggestions.push({fieldname:formElement.label,explanation:formElement.explanation,elementId:formElement.id,suggestionvalue:newValue,suggestiondisplayvalue:displayValue,suggestionpreview:isLong?plainText.substring(0,80)+"…":displayValue,isLong:isLong,disabledButtons:!htmlElement})})),suggestionContext}}var _default=Chat;return _exports.default=_default,_exports.default})); +define("block_ai_chat/components/chat",["exports","block_ai_chat/components/base_content","tiny_ai/utils","tiny_ai/editor_utils","tiny_ai/constants","core/templates","local_ai_manager/config","core/str","core/log","core/notification","block_ai_chat/utils","block_ai_chat/constants","block_ai_chat/dom_extractor","core/utils"],(function(_exports,_base_content,TinyAiUtils,_editor_utils,_constants,_templates,_config,_str,_log,_notification,_utils2,_constants2,DomExtractor,_utils3){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_base_content=_interopRequireDefault(_base_content),TinyAiUtils=_interopRequireWildcard(TinyAiUtils),_editor_utils=_interopRequireDefault(_editor_utils),_templates=_interopRequireDefault(_templates),_log=_interopRequireDefault(_log),DomExtractor=_interopRequireWildcard(DomExtractor);var _systemImportTransformerGlobalIdentifier="undefined"!=typeof window?window:"undefined"!=typeof self?self:"undefined"!=typeof global?global:{};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}}class Chat extends _base_content.default{static init(target){return new this({element:document.querySelector(target)})}create(){this.name="ChatComponent",this.selectors={MESSAGES:"[data-block_ai_chat-element='messages']",INPUT_TEXTAREA:"[data-block_ai_chat-element='inputtextarea']",SUBMIT_BUTTON:"[data-block_ai_chat-element='submitbutton']",LOADING_SPINNER_MESSAGE:"[data-block_ai_chat-element='loadingspinner']",TEMPORARY_PROMPT_MESSAGE:"[data-block_ai_chat-element='temporaryprompt']",TINY_AI_BUTTON:"[data-block_ai_chat-element='tinyaibutton']",OUTPUT_WRAPPER:"[data-block_ai_chat-element='outputwrapper']",CHAT_OUTPUT:"[data-block_ai_chat-element='chatoutput']",HISTORY_MARKER:"[data-block_ai_chat-element='historymarker']"},this._debouncedScrollToBottom=(0,_utils3.debounce)((messageElement=>this._scrollToBottom(messageElement)),350),this._debouncedFocusInputTextarea=(0,_utils3.debounce)((()=>this._focusInputTextarea()),350)}async stateReady(){this.reactive.dispatch("setView","chat")}getWatchers(){return[...super.getWatchers(),{watch:"messages:created",handler:this._addMessageToChatArea},{watch:"personas".concat(this.reactive.state.config.currentPersona,":deleted"),handler:this._removeCurrentPersona},{watch:"config.loadingState:updated",handler:this._handleLoadingStateUpdated}]}async _addMessageToChatArea(_ref){let{element:element}=_ref,placeholder=document.createElement("div");placeholder.setAttribute("data-id",element.id);let node=this.getElement(this.selectors.CHAT_OUTPUT);node.appendChild(placeholder);const responseIsAgentResponse="agent"===element.messageMode;let templateData={id:element.id,agentMode:responseIsAgentResponse,senderai:"ai"===element.sender,loading:!!element.hasOwnProperty("loading")&&element.loading};if(responseIsAgentResponse){const agentResponse=responseIsAgentResponse?this._getAgentAnswerTemplateContext(element.content):{};templateData={...templateData,...agentResponse}}else templateData.content=element.content;const newelement=(await this.renderComponent(placeholder,"block_ai_chat/components/message",templateData)).getElement();node.replaceChild(newelement,placeholder),this.reactive.dispatch("setMessageRendered",element.id,!0),this._highlightCodeBlocks(newelement),this._debouncedScrollToBottom(newelement),this._debouncedFocusInputTextarea()}async _highlightCodeBlocks(messageElement){if(messageElement.querySelector("pre code"))try{const PrismJS=await("function"==typeof _systemImportTransformerGlobalIdentifier.define&&_systemImportTransformerGlobalIdentifier.define.amd?new Promise((function(resolve,reject){_systemImportTransformerGlobalIdentifier.require(["filter_codehighlighter/prism"],resolve,reject)})):"undefined"!=typeof module&&module.exports&&"undefined"!=typeof require||"undefined"!=typeof module&&module.component&&_systemImportTransformerGlobalIdentifier.require&&"component"===_systemImportTransformerGlobalIdentifier.require.loader?Promise.resolve(require("filter_codehighlighter/prism")):Promise.resolve(_systemImportTransformerGlobalIdentifier["filter_codehighlighter/prism"]));PrismJS.plugins.customClass.prefix("prism-"),PrismJS.highlightAllUnder(messageElement)}catch(exception){_log.default.debug("block_ai_chat: code highlighting unavailable",exception)}}async _submitAiRequestListener(){const prompt=this.getElement(this.selectors.INPUT_TEXTAREA).value;if(""===prompt.trim()){const errorString=(0,_str.getString)("erroremptyprompt","block_ai_chat");return void await(0,_utils2.showErrorToast)(errorString)}const additionalOptions={};this.reactive.state.config.mode===_constants2.MODES.AGENT&&(additionalOptions.agentoptions={formelements:DomExtractor.extractDomElements(),pageid:document.body.id}),this.reactive.dispatch("submitAiRequest",prompt,additionalOptions)}async _handleLoadingStateUpdated(_ref2){let{element:element}=_ref2;const loadingSpinnerMessage={id:"loadingspinner",sender:"ai",loading:!0,agentMode:!1},temporaryPromptMessage={id:"temporaryprompt",sender:"user",content:this.formatPlainText(this.getElement(this.selectors.INPUT_TEXTAREA).value),agentMode:!1};if(element.loadingState){await this._addMessageToChatArea({element:temporaryPromptMessage}),await this._addMessageToChatArea({element:loadingSpinnerMessage});const inputTextarea=this.getElement(this.selectors.INPUT_TEXTAREA);inputTextarea.value="",inputTextarea.dispatchEvent(new Event("input"))}}formatPlainText(text){return text.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/ {2}/g,"  ").replace(/\n/g,"
")}_handleKeyDownOnInputTextarea(event){"Enter"!==event.key||event.shiftKey||(event.preventDefault(),this._submitAiRequestListener())}_removeCurrentPersona(){this.reactive.dispatch("selectCurrentPersona",0)}async _renderContent(){const{html:html,js:js}=await _templates.default.renderForPromise("block_ai_chat/chat_content",{conversationContextLimit:this.reactive.state.config.conversationContextLimit});_templates.default.replaceNodeContents(this.getElement(),html,js),await this._setupAfterContentRendering();const availabilityErrorMessage=await this.isAiChatAvailable();if(""!==availabilityErrorMessage){const notice=await(0,_str.getString)("notice","block_ai_chat");await(0,_notification.alert)(notice,availabilityErrorMessage),this.setElementLocked(this.getElement(this.selectors.INPUT_TEXTAREA),!0),this.setElementLocked(this.getElement(this.selectors.SUBMIT_BUTTON),!0),this.getElement(this.selectors.INPUT_TEXTAREA).disabled=!0,this.getElement(this.selectors.SUBMIT_BUTTON).disabled=!0}}async _setupAfterContentRendering(){this.reactive.dispatch("loadCurrentConversationMessages");const inputTextarea=this.getElement(this.selectors.INPUT_TEXTAREA),sendRequestButton=this.getElement(this.selectors.SUBMIT_BUTTON),tinyAiButton=this.getElement(this.selectors.TINY_AI_BUTTON),uniqid=Math.random().toString(16).slice(2);await TinyAiUtils.init(uniqid,this.reactive.state.static.contextid,_constants.constants.modalModes.standalone),this.addEventListener(tinyAiButton,"click",(async()=>{const selectionObject=window.getSelection();if(selectionObject.rangeCount>0){const range=selectionObject.getRangeAt(0),container=document.createElement("div");container.appendChild(range.cloneContents());const images=container.querySelectorAll("img");if(images.length>0&&images[0].src){const image=images[0],fetchResult=await fetch(image.src),data=await fetchResult.blob();TinyAiUtils.getDatamanager(uniqid).setSelectionImg(data)}selectionObject.toString()&&selectionObject.toString().length>0&&TinyAiUtils.getDatamanager(uniqid).setSelection(selectionObject.toString())}const editorUtils=new _editor_utils.default(uniqid,"block_ai_chat",this.reactive.state.static.contextid,this.reactive.state.static.userid,null);TinyAiUtils.setEditorUtils(uniqid,editorUtils),await editorUtils.displayDialogue()})),this.addEventListener(sendRequestButton,"click",this._submitAiRequestListener),this.addEventListener(inputTextarea,"keydown",this._handleKeyDownOnInputTextarea),this._enableTextAreaAutoResize(),this._debouncedScrollToBottom(),this._debouncedFocusInputTextarea()}getViewName(){return"chat"}_scrollToBottom(messageElement){const chatOutputWrapper=this.getElement(this.selectors.OUTPUT_WRAPPER);if(!messageElement||"message"!==messageElement.dataset.block_ai_chatComponent)return void(chatOutputWrapper.scrollTop=chatOutputWrapper.scrollHeight);const containerHeight=chatOutputWrapper.clientHeight;if(messageElement.offsetHeight>containerHeight){const previousMessage=messageElement.previousElementSibling;if(previousMessage){const promptVisibleHeight=(parseFloat(window.getComputedStyle(previousMessage).lineHeight)||24)*2,scrollPosition=messageElement.offsetTop-promptVisibleHeight;chatOutputWrapper.scrollTop=Math.max(0,scrollPosition)}else chatOutputWrapper.scrollTop=messageElement.offsetTop}else chatOutputWrapper.scrollTop=chatOutputWrapper.scrollHeight}_focusInputTextarea(){const inputTextarea=this.getElement(this.selectors.INPUT_TEXTAREA);requestAnimationFrame((()=>{inputTextarea.focus()}))}_enableTextAreaAutoResize(){const inputTextarea=this.getElement(this.selectors.INPUT_TEXTAREA);this.addEventListener(inputTextarea,"input",(()=>{inputTextarea.style.height="auto";const computedStyles=window.getComputedStyle(inputTextarea),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(inputTextarea.scrollHeight+borderTop+borderBottom,maxHeight);inputTextarea.style.height=newHeight+"px"}))}async isAiChatAvailable(){const contextid=this.reactive.state.static.contextid,aiConfig=await(0,_config.getAiConfig)(contextid,null,["chat"]);return"disabled"===aiConfig.availability.available?aiConfig.availability.errormessage:"disabled"===aiConfig.purposes[0].available||"hidden"===aiConfig.purposes[0].available?aiConfig.purposes[0].errormessage:""}_getAgentAnswerTemplateContext(content){const agentAnswer=JSON.parse(content),chatOutputIntroObject=agentAnswer.chatoutput.filter((object=>"intro"===object.type))[0],chatOutputOutroObject=agentAnswer.chatoutput.filter((object=>"outro"===object.type))[0],suggestionContext={intro:chatOutputIntroObject.text,suggestions:[],outro:chatOutputOutroObject.text};return agentAnswer.formelements.forEach((formElement=>{const htmlElement=document.getElementById(formElement.id);let newValue=formElement.newValue?formElement.newValue.toString().trim():"";0===newValue.length&&(newValue=0);const displayValue=formElement.suggestiondisplayvalue||"",plainText=displayValue.replace(/<[^>]*>/g,""),isLong=plainText.length>80;suggestionContext.suggestions.push({fieldname:formElement.label,explanation:formElement.explanation,elementId:formElement.id,suggestionvalue:newValue,suggestiondisplayvalue:displayValue,suggestionpreview:isLong?plainText.substring(0,80)+"…":displayValue,isLong:isLong,disabledButtons:!htmlElement})})),suggestionContext}}var _default=Chat;return _exports.default=_default,_exports.default})); //# sourceMappingURL=chat.min.js.map \ No newline at end of file diff --git a/amd/build/components/chat.min.js.map b/amd/build/components/chat.min.js.map index fd4e18b..40433ad 100644 --- a/amd/build/components/chat.min.js.map +++ b/amd/build/components/chat.min.js.map @@ -1 +1 @@ -{"version":3,"file":"chat.min.js","sources":["../../src/components/chat.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\nimport BaseContent from 'block_ai_chat/components/base_content';\nimport * as TinyAiUtils from 'tiny_ai/utils';\nimport TinyAiEditorUtils from 'tiny_ai/editor_utils';\nimport {constants as TinyAiConstants} from 'tiny_ai/constants';\nimport Templates from 'core/templates';\nimport {getAiConfig} from 'local_ai_manager/config';\nimport {getString} from 'core/str';\nimport Log from 'core/log';\nimport {alert as displayAlert} from 'core/notification';\nimport {showErrorToast} from 'block_ai_chat/utils';\nimport {MODES} from 'block_ai_chat/constants';\nimport * as DomExtractor from 'block_ai_chat/dom_extractor';\nimport {debounce} from 'core/utils';\n\nclass Chat extends BaseContent {\n /**\n * Function to initialize component, called by mustache template.\n *\n * @param {*} target The id of the HTMLElement to attach to\n * @returns {BaseComponent} New component attached to the HTMLElement represented by target\n */\n static init(target) {\n let element = document.querySelector(target);\n return new this({\n element: element,\n });\n }\n\n /**\n * It is important to follow some conventions while you write components. This way all components\n * will be implemented in a similar way and anybody will be able to understand how it works.\n *\n * All the component definition should be initialized on the \"create\" method.\n */\n create() {\n this.name = 'ChatComponent';\n this.selectors = {\n MESSAGES: `[data-block_ai_chat-element='messages']`,\n INPUT_TEXTAREA: `[data-block_ai_chat-element='inputtextarea']`,\n SUBMIT_BUTTON: `[data-block_ai_chat-element='submitbutton']`,\n LOADING_SPINNER_MESSAGE: `[data-block_ai_chat-element='loadingspinner']`,\n TEMPORARY_PROMPT_MESSAGE: `[data-block_ai_chat-element='temporaryprompt']`,\n TINY_AI_BUTTON: `[data-block_ai_chat-element='tinyaibutton']`,\n OUTPUT_WRAPPER: `[data-block_ai_chat-element='outputwrapper']`,\n CHAT_OUTPUT: `[data-block_ai_chat-element='chatoutput']`,\n HISTORY_MARKER: `[data-block_ai_chat-element='historymarker']`,\n };\n this._debouncedScrollToBottom = debounce((messageElement) => this._scrollToBottom(messageElement), 350);\n this._debouncedFocusInputTextarea = debounce(() => this._focusInputTextarea(), 350);\n }\n\n /**\n * Initial state ready method.\n *\n * Initially calls the setView action to set the view to 'chat' once ready to trigger the first\n * rendering of itself.\n */\n async stateReady() {\n this.reactive.dispatch('setView', 'chat');\n }\n\n getWatchers() {\n return [\n ...super.getWatchers(),\n {watch: `messages:created`, handler: this._addMessageToChatArea},\n {watch: `personas${this.reactive.state.config.currentPersona}:deleted`, handler: this._removeCurrentPersona},\n {watch: `config.loadingState:updated`, handler: this._handleLoadingStateUpdated},\n ];\n }\n\n async _addMessageToChatArea({element}) {\n let placeholder = document.createElement('div');\n placeholder.setAttribute('data-id', element.id);\n let node = this.getElement(this.selectors.CHAT_OUTPUT);\n node.appendChild(placeholder);\n\n const responseIsAgentResponse = element.messageMode === 'agent';\n\n let templateData = {\n id: element.id,\n agentMode: responseIsAgentResponse,\n senderai: element.sender === 'ai',\n loading: element.hasOwnProperty('loading') ? element.loading : false,\n };\n if (responseIsAgentResponse) {\n const agentResponse = responseIsAgentResponse ? this._getAgentAnswerTemplateContext(element.content) : {};\n templateData = {...templateData, ...agentResponse};\n } else {\n templateData.content = element.content;\n }\n const newcomponent = await this.renderComponent(placeholder, 'block_ai_chat/components/message', templateData);\n const newelement = newcomponent.getElement();\n node.replaceChild(newelement, placeholder);\n this.reactive.dispatch('setMessageRendered', element.id, true);\n this._highlightCodeBlocks(newelement);\n\n // Pass the element to enable smart scrolling for long responses.\n this._debouncedScrollToBottom(newelement);\n this._debouncedFocusInputTextarea();\n }\n\n /**\n * Applies syntax highlighting to the code blocks of a freshly inserted message.\n *\n * The chat output is formatted with filters disabled, so filter_codehighlighter never runs and never\n * loads Prism. Even on pages where it did run, its initialization highlights only once on page load,\n * which does not cover messages arriving via AJAX.\n *\n * Workaround: we deep-include the filter's Prism module here until MDL-88910\n * (https://moodle.atlassian.net/browse/MDL-88910) ships the client-side rehighlight in the filter itself.\n *\n * @param {HTMLElement} messageElement the message node that has just been added to the DOM\n */\n async _highlightCodeBlocks(messageElement) {\n if (!messageElement.querySelector('pre code')) {\n return;\n }\n try {\n const PrismJS = await import('filter_codehighlighter/prism');\n PrismJS.plugins.customClass.prefix('prism-');\n PrismJS.highlightAllUnder(messageElement);\n } catch (exception) {\n // The codehighlighter filter is an optional plugin, so code blocks just stay unhighlighted.\n Log.debug('block_ai_chat: code highlighting unavailable', exception);\n }\n }\n\n async _submitAiRequestListener() {\n const textarea = this.getElement(this.selectors.INPUT_TEXTAREA);\n const prompt = textarea.value;\n if (prompt.trim() === '') {\n const errorString = getString('erroremptyprompt', 'block_ai_chat');\n await showErrorToast(errorString);\n return;\n }\n const additionalOptions = {};\n if (this.reactive.state.config.mode === MODES.AGENT) {\n additionalOptions.agentoptions = {\n formelements: DomExtractor.extractDomElements(),\n pageid: document.body.id\n };\n }\n this.reactive.dispatch('submitAiRequest', prompt, additionalOptions);\n }\n\n async _handleLoadingStateUpdated({element}) {\n const loadingSpinnerMessage = {\n 'id': 'loadingspinner',\n 'sender': 'ai',\n 'loading': true,\n 'agentMode': false\n };\n\n const temporaryPromptMessage = {\n 'id': 'temporaryprompt',\n 'sender': 'user',\n // Format the raw prompt like the persisted message (server side format_text(FORMAT_PLAIN)), so\n // the temporary message keeps line breaks and indentation and is safe for unescaped rendering.\n 'content': this.formatPlainText(this.getElement(this.selectors.INPUT_TEXTAREA).value),\n 'agentMode': false\n };\n\n if (element.loadingState) {\n await this._addMessageToChatArea({element: temporaryPromptMessage});\n await this._addMessageToChatArea({element: loadingSpinnerMessage});\n const inputTextarea = this.getElement(this.selectors.INPUT_TEXTAREA);\n inputTextarea.value = '';\n // Very hacky, but we need to fire this event manually to trigger the auto-resize.\n inputTextarea.dispatchEvent(new Event('input'));\n }\n }\n\n /**\n * Formats a raw prompt for display, mirroring the server side format_text(..., FORMAT_PLAIN): escapes\n * HTML special characters, keeps indentation via non-breaking spaces and turns line breaks into
.\n *\n * @param {string} text the raw text to format\n * @returns {string} the formatted, display-safe text\n */\n formatPlainText(text) {\n return text\n .replace(/&/g, '&')\n .replace(//g, '>')\n .replace(/\"/g, '"')\n // HTML collapses multiple spaces into one, so replace each pair by a protected space to keep indentation.\n .replace(/ {2}/g, '  ')\n .replace(/\\n/g, '
');\n }\n\n _handleKeyDownOnInputTextarea(event) {\n if (event.key === 'Enter' && !event.shiftKey) {\n event.preventDefault();\n this._submitAiRequestListener();\n }\n }\n\n _removeCurrentPersona() {\n this.reactive.dispatch('selectCurrentPersona', 0);\n }\n\n async _renderContent() {\n const {html, js} = await Templates.renderForPromise(\n 'block_ai_chat/chat_content',\n {conversationContextLimit: this.reactive.state.config.conversationContextLimit}\n );\n Templates.replaceNodeContents(this.getElement(), html, js);\n await this._setupAfterContentRendering();\n const availabilityErrorMessage = await this.isAiChatAvailable();\n if (availabilityErrorMessage !== '') {\n const notice = await getString('notice', 'block_ai_chat');\n await displayAlert(notice, availabilityErrorMessage);\n this.setElementLocked(this.getElement(this.selectors.INPUT_TEXTAREA), true);\n this.setElementLocked(this.getElement(this.selectors.SUBMIT_BUTTON), true);\n this.getElement(this.selectors.INPUT_TEXTAREA).disabled = true;\n this.getElement(this.selectors.SUBMIT_BUTTON).disabled = true;\n }\n }\n\n async _setupAfterContentRendering() {\n this.reactive.dispatch('loadCurrentConversationMessages');\n const inputTextarea = this.getElement(this.selectors.INPUT_TEXTAREA);\n const sendRequestButton = this.getElement(this.selectors.SUBMIT_BUTTON);\n const tinyAiButton = this.getElement(this.selectors.TINY_AI_BUTTON);\n const uniqid = Math.random().toString(16).slice(2);\n\n await TinyAiUtils.init(uniqid, this.reactive.state.static.contextid, TinyAiConstants.modalModes.standalone);\n this.addEventListener(tinyAiButton, 'click', async() => {\n // We try to find selected text or images and inject it into the AI tools.\n const selectionObject = window.getSelection();\n if (selectionObject.rangeCount > 0) {\n // Safari browser does not really comply with MDN standard and sometimes has\n // rangeCount === 0. So we have to check for this to avoid running into an error.\n const range = selectionObject.getRangeAt(0);\n const container = document.createElement('div');\n container.appendChild(range.cloneContents());\n const images = container.querySelectorAll('img');\n if (images.length > 0 && images[0].src) {\n // If there are more than one we just use the first one.\n const image = images[0];\n // This should work for both external and data urls.\n const fetchResult = await fetch(image.src);\n const data = await fetchResult.blob();\n TinyAiUtils.getDatamanager(uniqid).setSelectionImg(data);\n }\n\n // If currently there is text selected we inject it.\n if (selectionObject.toString() && selectionObject.toString().length > 0) {\n TinyAiUtils.getDatamanager(uniqid).setSelection(selectionObject.toString());\n }\n }\n\n const editorUtils = new TinyAiEditorUtils(\n uniqid,\n 'block_ai_chat',\n this.reactive.state.static.contextid,\n this.reactive.state.static.userid,\n null\n );\n TinyAiUtils.setEditorUtils(uniqid, editorUtils);\n await editorUtils.displayDialogue();\n });\n\n this.addEventListener(sendRequestButton, 'click', this._submitAiRequestListener);\n this.addEventListener(inputTextarea, 'keydown', this._handleKeyDownOnInputTextarea);\n\n this._enableTextAreaAutoResize();\n this._debouncedScrollToBottom();\n this._debouncedFocusInputTextarea();\n }\n\n getViewName() {\n return 'chat';\n }\n\n /**\n * Scrolls the chat output to show the latest message optimally.\n *\n * For long responses that don't fit in the container, scrolls so that\n * the start of the response is visible while keeping the last two lines\n * of the user's prompt visible for orientation.\n * For short responses, scrolls to the bottom as usual.\n *\n * @param {HTMLElement} messageElement The message element to scroll to\n */\n _scrollToBottom(messageElement) {\n const chatOutputWrapper = this.getElement(this.selectors.OUTPUT_WRAPPER);\n\n // Guard: Check if the passed element is a valid message component.\n if (!messageElement || messageElement.dataset.block_ai_chatComponent !== 'message') {\n chatOutputWrapper.scrollTop = chatOutputWrapper.scrollHeight;\n return;\n }\n\n const containerHeight = chatOutputWrapper.clientHeight;\n const messageHeight = messageElement.offsetHeight;\n\n // Check if the message is taller than the container (long response).\n if (messageHeight > containerHeight) {\n // Get the previous sibling (user's prompt message) if it exists.\n const previousMessage = messageElement.previousElementSibling;\n if (previousMessage) {\n // Read the actual line height from the previous message's computed styles.\n const lineHeight = parseFloat(window.getComputedStyle(previousMessage).lineHeight) || 24;\n // Number of prompt lines to keep visible for user orientation.\n const visiblePromptLines = 2;\n const promptVisibleHeight = lineHeight * visiblePromptLines;\n\n // Scroll so that the last two lines of the prompt and the start of the response are visible.\n const scrollPosition = messageElement.offsetTop - promptVisibleHeight;\n chatOutputWrapper.scrollTop = Math.max(0, scrollPosition);\n } else {\n // No previous message, scroll to the start of this message.\n chatOutputWrapper.scrollTop = messageElement.offsetTop;\n }\n } else {\n // Short message - scroll to bottom to show the complete message.\n chatOutputWrapper.scrollTop = chatOutputWrapper.scrollHeight;\n }\n }\n\n _focusInputTextarea() {\n const inputTextarea = this.getElement(this.selectors.INPUT_TEXTAREA);\n requestAnimationFrame(() => {\n inputTextarea.focus();\n });\n }\n\n _enableTextAreaAutoResize() {\n const inputTextarea = this.getElement(this.selectors.INPUT_TEXTAREA);\n this.addEventListener(inputTextarea, 'input', () => {\n // Handle autogrow/-shrink.\n // Reset the height to auto to get the correct scrollHeight.\n inputTextarea.style.height = 'auto';\n\n // Fetch the computed styles.\n const computedStyles = window.getComputedStyle(inputTextarea);\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(inputTextarea.scrollHeight + borderTop + borderBottom, maxHeight);\n\n // Set the new height.\n inputTextarea.style.height = newHeight + 'px';\n });\n }\n\n /**\n * Is user allowed to use the chatbot.\n *\n * @returns {string} Empty string if available, error message if not.\n */\n async isAiChatAvailable() {\n const contextid = this.reactive.state.static.contextid;\n const aiConfig = await getAiConfig(contextid, null, ['chat']);\n if (aiConfig.availability.available === 'disabled') {\n return aiConfig.availability.errormessage;\n }\n if (aiConfig.purposes[0].available === 'disabled') {\n return aiConfig.purposes[0].errormessage;\n }\n return '';\n }\n\n /**\n * Get the template context for agent answer rendering.\n *\n * @param {string} content The JSON content from the AI response\n * @returns {Object} Template context for agent suggestions\n */\n _getAgentAnswerTemplateContext(content) {\n const agentAnswer = JSON.parse(content);\n const chatOutputIntroObject = agentAnswer.chatoutput.filter(object => object.type === 'intro')[0];\n const chatOutputOutroObject = agentAnswer.chatoutput.filter(object => object.type === 'outro')[0];\n const maxPreviewLength = 80;\n const suggestionContext = {\n intro: chatOutputIntroObject.text,\n suggestions: [],\n outro: chatOutputOutroObject.text\n };\n agentAnswer.formelements.forEach((formElement) => {\n const htmlElement = document.getElementById(formElement.id);\n let newValue = formElement.newValue ? formElement.newValue.toString().trim() : '';\n if (newValue.length === 0) {\n newValue = 0;\n }\n // The sanitized display value is the only value safe for the raw template rendering.\n // An empty value means the sanitizer rejected the whole suggestion.\n const displayValue = formElement.suggestiondisplayvalue || '';\n // Strip HTML tags to get plain text for computing a truncated preview of long suggestions.\n const plainText = displayValue.replace(/<[^>]*>/g, '');\n const isLong = plainText.length > maxPreviewLength;\n suggestionContext.suggestions.push({\n fieldname: formElement.label,\n explanation: formElement.explanation,\n elementId: formElement.id,\n suggestionvalue: newValue,\n suggestiondisplayvalue: displayValue,\n suggestionpreview: isLong ? plainText.substring(0, maxPreviewLength) + '…' : displayValue,\n isLong: isLong,\n disabledButtons: !htmlElement\n });\n });\n return suggestionContext;\n }\n}\n\nexport default Chat;\n"],"names":["Chat","BaseContent","target","this","element","document","querySelector","create","name","selectors","MESSAGES","INPUT_TEXTAREA","SUBMIT_BUTTON","LOADING_SPINNER_MESSAGE","TEMPORARY_PROMPT_MESSAGE","TINY_AI_BUTTON","OUTPUT_WRAPPER","CHAT_OUTPUT","HISTORY_MARKER","_debouncedScrollToBottom","messageElement","_scrollToBottom","_debouncedFocusInputTextarea","_focusInputTextarea","reactive","dispatch","getWatchers","super","watch","handler","_addMessageToChatArea","state","config","currentPersona","_removeCurrentPersona","_handleLoadingStateUpdated","placeholder","createElement","setAttribute","id","node","getElement","appendChild","responseIsAgentResponse","messageMode","templateData","agentMode","senderai","sender","loading","hasOwnProperty","agentResponse","_getAgentAnswerTemplateContext","content","newelement","renderComponent","replaceChild","_highlightCodeBlocks","PrismJS","plugins","customClass","prefix","highlightAllUnder","exception","debug","prompt","value","trim","errorString","additionalOptions","mode","MODES","AGENT","agentoptions","formelements","DomExtractor","extractDomElements","pageid","body","loadingSpinnerMessage","temporaryPromptMessage","formatPlainText","loadingState","inputTextarea","dispatchEvent","Event","text","replace","_handleKeyDownOnInputTextarea","event","key","shiftKey","preventDefault","_submitAiRequestListener","html","js","Templates","renderForPromise","conversationContextLimit","replaceNodeContents","_setupAfterContentRendering","availabilityErrorMessage","isAiChatAvailable","notice","setElementLocked","disabled","sendRequestButton","tinyAiButton","uniqid","Math","random","toString","slice","TinyAiUtils","init","static","contextid","TinyAiConstants","modalModes","standalone","addEventListener","async","selectionObject","window","getSelection","rangeCount","range","getRangeAt","container","cloneContents","images","querySelectorAll","length","src","image","fetchResult","fetch","data","blob","getDatamanager","setSelectionImg","setSelection","editorUtils","TinyAiEditorUtils","userid","setEditorUtils","displayDialogue","_enableTextAreaAutoResize","getViewName","chatOutputWrapper","dataset","block_ai_chatComponent","scrollTop","scrollHeight","containerHeight","clientHeight","offsetHeight","previousMessage","previousElementSibling","promptVisibleHeight","parseFloat","getComputedStyle","lineHeight","scrollPosition","offsetTop","max","requestAnimationFrame","focus","style","height","computedStyles","paddingTop","paddingBottom","borderTop","borderTopWidth","borderBottom","borderBottomWidth","maxHeight","newHeight","min","aiConfig","availability","available","errormessage","purposes","agentAnswer","JSON","parse","chatOutputIntroObject","chatoutput","filter","object","type","chatOutputOutroObject","suggestionContext","intro","suggestions","outro","forEach","formElement","htmlElement","getElementById","newValue","displayValue","suggestiondisplayvalue","plainText","isLong","push","fieldname","label","explanation","elementId","suggestionvalue","suggestionpreview","substring","disabledButtons"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"chat.min.js","sources":["../../src/components/chat.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\nimport BaseContent from 'block_ai_chat/components/base_content';\nimport * as TinyAiUtils from 'tiny_ai/utils';\nimport TinyAiEditorUtils from 'tiny_ai/editor_utils';\nimport {constants as TinyAiConstants} from 'tiny_ai/constants';\nimport Templates from 'core/templates';\nimport {getAiConfig} from 'local_ai_manager/config';\nimport {getString} from 'core/str';\nimport Log from 'core/log';\nimport {alert as displayAlert} from 'core/notification';\nimport {showErrorToast} from 'block_ai_chat/utils';\nimport {MODES} from 'block_ai_chat/constants';\nimport * as DomExtractor from 'block_ai_chat/dom_extractor';\nimport {debounce} from 'core/utils';\n\nclass Chat extends BaseContent {\n /**\n * Function to initialize component, called by mustache template.\n *\n * @param {*} target The id of the HTMLElement to attach to\n * @returns {BaseComponent} New component attached to the HTMLElement represented by target\n */\n static init(target) {\n let element = document.querySelector(target);\n return new this({\n element: element,\n });\n }\n\n /**\n * It is important to follow some conventions while you write components. This way all components\n * will be implemented in a similar way and anybody will be able to understand how it works.\n *\n * All the component definition should be initialized on the \"create\" method.\n */\n create() {\n this.name = 'ChatComponent';\n this.selectors = {\n MESSAGES: `[data-block_ai_chat-element='messages']`,\n INPUT_TEXTAREA: `[data-block_ai_chat-element='inputtextarea']`,\n SUBMIT_BUTTON: `[data-block_ai_chat-element='submitbutton']`,\n LOADING_SPINNER_MESSAGE: `[data-block_ai_chat-element='loadingspinner']`,\n TEMPORARY_PROMPT_MESSAGE: `[data-block_ai_chat-element='temporaryprompt']`,\n TINY_AI_BUTTON: `[data-block_ai_chat-element='tinyaibutton']`,\n OUTPUT_WRAPPER: `[data-block_ai_chat-element='outputwrapper']`,\n CHAT_OUTPUT: `[data-block_ai_chat-element='chatoutput']`,\n HISTORY_MARKER: `[data-block_ai_chat-element='historymarker']`,\n };\n this._debouncedScrollToBottom = debounce((messageElement) => this._scrollToBottom(messageElement), 350);\n this._debouncedFocusInputTextarea = debounce(() => this._focusInputTextarea(), 350);\n }\n\n /**\n * Initial state ready method.\n *\n * Initially calls the setView action to set the view to 'chat' once ready to trigger the first\n * rendering of itself.\n */\n async stateReady() {\n this.reactive.dispatch('setView', 'chat');\n }\n\n getWatchers() {\n return [\n ...super.getWatchers(),\n {watch: `messages:created`, handler: this._addMessageToChatArea},\n {watch: `personas${this.reactive.state.config.currentPersona}:deleted`, handler: this._removeCurrentPersona},\n {watch: `config.loadingState:updated`, handler: this._handleLoadingStateUpdated},\n ];\n }\n\n async _addMessageToChatArea({element}) {\n let placeholder = document.createElement('div');\n placeholder.setAttribute('data-id', element.id);\n let node = this.getElement(this.selectors.CHAT_OUTPUT);\n node.appendChild(placeholder);\n\n const responseIsAgentResponse = element.messageMode === 'agent';\n\n let templateData = {\n id: element.id,\n agentMode: responseIsAgentResponse,\n senderai: element.sender === 'ai',\n loading: element.hasOwnProperty('loading') ? element.loading : false,\n };\n if (responseIsAgentResponse) {\n const agentResponse = responseIsAgentResponse ? this._getAgentAnswerTemplateContext(element.content) : {};\n templateData = {...templateData, ...agentResponse};\n } else {\n templateData.content = element.content;\n }\n const newcomponent = await this.renderComponent(placeholder, 'block_ai_chat/components/message', templateData);\n const newelement = newcomponent.getElement();\n node.replaceChild(newelement, placeholder);\n this.reactive.dispatch('setMessageRendered', element.id, true);\n this._highlightCodeBlocks(newelement);\n\n // Pass the element to enable smart scrolling for long responses.\n this._debouncedScrollToBottom(newelement);\n this._debouncedFocusInputTextarea();\n }\n\n /**\n * Applies syntax highlighting to the code blocks of a freshly inserted message.\n *\n * The chat output is formatted with filters disabled, so filter_codehighlighter never runs and never\n * loads Prism. Even on pages where it did run, its initialization highlights only once on page load,\n * which does not cover messages arriving via AJAX.\n *\n * Workaround: we deep-include the filter's Prism module here until MDL-88910\n * (https://moodle.atlassian.net/browse/MDL-88910) ships the client-side rehighlight in the filter itself.\n *\n * @param {HTMLElement} messageElement the message node that has just been added to the DOM\n */\n async _highlightCodeBlocks(messageElement) {\n if (!messageElement.querySelector('pre code')) {\n return;\n }\n try {\n const PrismJS = await import('filter_codehighlighter/prism');\n PrismJS.plugins.customClass.prefix('prism-');\n PrismJS.highlightAllUnder(messageElement);\n } catch (exception) {\n // The codehighlighter filter is an optional plugin, so code blocks just stay unhighlighted.\n Log.debug('block_ai_chat: code highlighting unavailable', exception);\n }\n }\n\n async _submitAiRequestListener() {\n const textarea = this.getElement(this.selectors.INPUT_TEXTAREA);\n const prompt = textarea.value;\n if (prompt.trim() === '') {\n const errorString = getString('erroremptyprompt', 'block_ai_chat');\n await showErrorToast(errorString);\n return;\n }\n const additionalOptions = {};\n if (this.reactive.state.config.mode === MODES.AGENT) {\n additionalOptions.agentoptions = {\n formelements: DomExtractor.extractDomElements(),\n pageid: document.body.id\n };\n }\n this.reactive.dispatch('submitAiRequest', prompt, additionalOptions);\n }\n\n async _handleLoadingStateUpdated({element}) {\n const loadingSpinnerMessage = {\n 'id': 'loadingspinner',\n 'sender': 'ai',\n 'loading': true,\n 'agentMode': false\n };\n\n const temporaryPromptMessage = {\n 'id': 'temporaryprompt',\n 'sender': 'user',\n // Format the raw prompt like the persisted message (server side format_text(FORMAT_PLAIN)), so\n // the temporary message keeps line breaks and indentation and is safe for unescaped rendering.\n 'content': this.formatPlainText(this.getElement(this.selectors.INPUT_TEXTAREA).value),\n 'agentMode': false\n };\n\n if (element.loadingState) {\n await this._addMessageToChatArea({element: temporaryPromptMessage});\n await this._addMessageToChatArea({element: loadingSpinnerMessage});\n const inputTextarea = this.getElement(this.selectors.INPUT_TEXTAREA);\n inputTextarea.value = '';\n // Very hacky, but we need to fire this event manually to trigger the auto-resize.\n inputTextarea.dispatchEvent(new Event('input'));\n }\n }\n\n /**\n * Formats a raw prompt for display, mirroring the server side format_text(..., FORMAT_PLAIN): escapes\n * HTML special characters, keeps indentation via non-breaking spaces and turns line breaks into
.\n *\n * @param {string} text the raw text to format\n * @returns {string} the formatted, display-safe text\n */\n formatPlainText(text) {\n return text\n .replace(/&/g, '&')\n .replace(//g, '>')\n .replace(/\"/g, '"')\n // HTML collapses multiple spaces into one, so replace each pair by a protected space to keep indentation.\n .replace(/ {2}/g, '  ')\n .replace(/\\n/g, '
');\n }\n\n _handleKeyDownOnInputTextarea(event) {\n if (event.key === 'Enter' && !event.shiftKey) {\n event.preventDefault();\n this._submitAiRequestListener();\n }\n }\n\n _removeCurrentPersona() {\n this.reactive.dispatch('selectCurrentPersona', 0);\n }\n\n async _renderContent() {\n const {html, js} = await Templates.renderForPromise(\n 'block_ai_chat/chat_content',\n {conversationContextLimit: this.reactive.state.config.conversationContextLimit}\n );\n Templates.replaceNodeContents(this.getElement(), html, js);\n await this._setupAfterContentRendering();\n const availabilityErrorMessage = await this.isAiChatAvailable();\n if (availabilityErrorMessage !== '') {\n const notice = await getString('notice', 'block_ai_chat');\n await displayAlert(notice, availabilityErrorMessage);\n this.setElementLocked(this.getElement(this.selectors.INPUT_TEXTAREA), true);\n this.setElementLocked(this.getElement(this.selectors.SUBMIT_BUTTON), true);\n this.getElement(this.selectors.INPUT_TEXTAREA).disabled = true;\n this.getElement(this.selectors.SUBMIT_BUTTON).disabled = true;\n }\n }\n\n async _setupAfterContentRendering() {\n this.reactive.dispatch('loadCurrentConversationMessages');\n const inputTextarea = this.getElement(this.selectors.INPUT_TEXTAREA);\n const sendRequestButton = this.getElement(this.selectors.SUBMIT_BUTTON);\n const tinyAiButton = this.getElement(this.selectors.TINY_AI_BUTTON);\n const uniqid = Math.random().toString(16).slice(2);\n\n await TinyAiUtils.init(uniqid, this.reactive.state.static.contextid, TinyAiConstants.modalModes.standalone);\n this.addEventListener(tinyAiButton, 'click', async() => {\n // We try to find selected text or images and inject it into the AI tools.\n const selectionObject = window.getSelection();\n if (selectionObject.rangeCount > 0) {\n // Safari browser does not really comply with MDN standard and sometimes has\n // rangeCount === 0. So we have to check for this to avoid running into an error.\n const range = selectionObject.getRangeAt(0);\n const container = document.createElement('div');\n container.appendChild(range.cloneContents());\n const images = container.querySelectorAll('img');\n if (images.length > 0 && images[0].src) {\n // If there are more than one we just use the first one.\n const image = images[0];\n // This should work for both external and data urls.\n const fetchResult = await fetch(image.src);\n const data = await fetchResult.blob();\n TinyAiUtils.getDatamanager(uniqid).setSelectionImg(data);\n }\n\n // If currently there is text selected we inject it.\n if (selectionObject.toString() && selectionObject.toString().length > 0) {\n TinyAiUtils.getDatamanager(uniqid).setSelection(selectionObject.toString());\n }\n }\n\n const editorUtils = new TinyAiEditorUtils(\n uniqid,\n 'block_ai_chat',\n this.reactive.state.static.contextid,\n this.reactive.state.static.userid,\n null\n );\n TinyAiUtils.setEditorUtils(uniqid, editorUtils);\n await editorUtils.displayDialogue();\n });\n\n this.addEventListener(sendRequestButton, 'click', this._submitAiRequestListener);\n this.addEventListener(inputTextarea, 'keydown', this._handleKeyDownOnInputTextarea);\n\n this._enableTextAreaAutoResize();\n this._debouncedScrollToBottom();\n this._debouncedFocusInputTextarea();\n }\n\n getViewName() {\n return 'chat';\n }\n\n /**\n * Scrolls the chat output to show the latest message optimally.\n *\n * For long responses that don't fit in the container, scrolls so that\n * the start of the response is visible while keeping the last two lines\n * of the user's prompt visible for orientation.\n * For short responses, scrolls to the bottom as usual.\n *\n * @param {HTMLElement} messageElement The message element to scroll to\n */\n _scrollToBottom(messageElement) {\n const chatOutputWrapper = this.getElement(this.selectors.OUTPUT_WRAPPER);\n\n // Guard: Check if the passed element is a valid message component.\n if (!messageElement || messageElement.dataset.block_ai_chatComponent !== 'message') {\n chatOutputWrapper.scrollTop = chatOutputWrapper.scrollHeight;\n return;\n }\n\n const containerHeight = chatOutputWrapper.clientHeight;\n const messageHeight = messageElement.offsetHeight;\n\n // Check if the message is taller than the container (long response).\n if (messageHeight > containerHeight) {\n // Get the previous sibling (user's prompt message) if it exists.\n const previousMessage = messageElement.previousElementSibling;\n if (previousMessage) {\n // Read the actual line height from the previous message's computed styles.\n const lineHeight = parseFloat(window.getComputedStyle(previousMessage).lineHeight) || 24;\n // Number of prompt lines to keep visible for user orientation.\n const visiblePromptLines = 2;\n const promptVisibleHeight = lineHeight * visiblePromptLines;\n\n // Scroll so that the last two lines of the prompt and the start of the response are visible.\n const scrollPosition = messageElement.offsetTop - promptVisibleHeight;\n chatOutputWrapper.scrollTop = Math.max(0, scrollPosition);\n } else {\n // No previous message, scroll to the start of this message.\n chatOutputWrapper.scrollTop = messageElement.offsetTop;\n }\n } else {\n // Short message - scroll to bottom to show the complete message.\n chatOutputWrapper.scrollTop = chatOutputWrapper.scrollHeight;\n }\n }\n\n _focusInputTextarea() {\n const inputTextarea = this.getElement(this.selectors.INPUT_TEXTAREA);\n requestAnimationFrame(() => {\n inputTextarea.focus();\n });\n }\n\n _enableTextAreaAutoResize() {\n const inputTextarea = this.getElement(this.selectors.INPUT_TEXTAREA);\n this.addEventListener(inputTextarea, 'input', () => {\n // Handle autogrow/-shrink.\n // Reset the height to auto to get the correct scrollHeight.\n inputTextarea.style.height = 'auto';\n\n // Fetch the computed styles.\n const computedStyles = window.getComputedStyle(inputTextarea);\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(inputTextarea.scrollHeight + borderTop + borderBottom, maxHeight);\n\n // Set the new height.\n inputTextarea.style.height = newHeight + 'px';\n });\n }\n\n /**\n * Is user allowed to use the chatbot.\n *\n * @returns {string} Empty string if available, error message if not.\n */\n async isAiChatAvailable() {\n const contextid = this.reactive.state.static.contextid;\n const aiConfig = await getAiConfig(contextid, null, ['chat']);\n if (aiConfig.availability.available === 'disabled') {\n return aiConfig.availability.errormessage;\n }\n if (aiConfig.purposes[0].available === 'disabled' || aiConfig.purposes[0].available === 'hidden') {\n return aiConfig.purposes[0].errormessage;\n }\n return '';\n }\n\n /**\n * Get the template context for agent answer rendering.\n *\n * @param {string} content The JSON content from the AI response\n * @returns {Object} Template context for agent suggestions\n */\n _getAgentAnswerTemplateContext(content) {\n const agentAnswer = JSON.parse(content);\n const chatOutputIntroObject = agentAnswer.chatoutput.filter(object => object.type === 'intro')[0];\n const chatOutputOutroObject = agentAnswer.chatoutput.filter(object => object.type === 'outro')[0];\n const maxPreviewLength = 80;\n const suggestionContext = {\n intro: chatOutputIntroObject.text,\n suggestions: [],\n outro: chatOutputOutroObject.text\n };\n agentAnswer.formelements.forEach((formElement) => {\n const htmlElement = document.getElementById(formElement.id);\n let newValue = formElement.newValue ? formElement.newValue.toString().trim() : '';\n if (newValue.length === 0) {\n newValue = 0;\n }\n // The sanitized display value is the only value safe for the raw template rendering.\n // An empty value means the sanitizer rejected the whole suggestion.\n const displayValue = formElement.suggestiondisplayvalue || '';\n // Strip HTML tags to get plain text for computing a truncated preview of long suggestions.\n const plainText = displayValue.replace(/<[^>]*>/g, '');\n const isLong = plainText.length > maxPreviewLength;\n suggestionContext.suggestions.push({\n fieldname: formElement.label,\n explanation: formElement.explanation,\n elementId: formElement.id,\n suggestionvalue: newValue,\n suggestiondisplayvalue: displayValue,\n suggestionpreview: isLong ? plainText.substring(0, maxPreviewLength) + '…' : displayValue,\n isLong: isLong,\n disabledButtons: !htmlElement\n });\n });\n return suggestionContext;\n }\n}\n\nexport default Chat;\n"],"names":["Chat","BaseContent","target","this","element","document","querySelector","create","name","selectors","MESSAGES","INPUT_TEXTAREA","SUBMIT_BUTTON","LOADING_SPINNER_MESSAGE","TEMPORARY_PROMPT_MESSAGE","TINY_AI_BUTTON","OUTPUT_WRAPPER","CHAT_OUTPUT","HISTORY_MARKER","_debouncedScrollToBottom","messageElement","_scrollToBottom","_debouncedFocusInputTextarea","_focusInputTextarea","reactive","dispatch","getWatchers","super","watch","handler","_addMessageToChatArea","state","config","currentPersona","_removeCurrentPersona","_handleLoadingStateUpdated","placeholder","createElement","setAttribute","id","node","getElement","appendChild","responseIsAgentResponse","messageMode","templateData","agentMode","senderai","sender","loading","hasOwnProperty","agentResponse","_getAgentAnswerTemplateContext","content","newelement","renderComponent","replaceChild","_highlightCodeBlocks","PrismJS","plugins","customClass","prefix","highlightAllUnder","exception","debug","prompt","value","trim","errorString","additionalOptions","mode","MODES","AGENT","agentoptions","formelements","DomExtractor","extractDomElements","pageid","body","loadingSpinnerMessage","temporaryPromptMessage","formatPlainText","loadingState","inputTextarea","dispatchEvent","Event","text","replace","_handleKeyDownOnInputTextarea","event","key","shiftKey","preventDefault","_submitAiRequestListener","html","js","Templates","renderForPromise","conversationContextLimit","replaceNodeContents","_setupAfterContentRendering","availabilityErrorMessage","isAiChatAvailable","notice","setElementLocked","disabled","sendRequestButton","tinyAiButton","uniqid","Math","random","toString","slice","TinyAiUtils","init","static","contextid","TinyAiConstants","modalModes","standalone","addEventListener","async","selectionObject","window","getSelection","rangeCount","range","getRangeAt","container","cloneContents","images","querySelectorAll","length","src","image","fetchResult","fetch","data","blob","getDatamanager","setSelectionImg","setSelection","editorUtils","TinyAiEditorUtils","userid","setEditorUtils","displayDialogue","_enableTextAreaAutoResize","getViewName","chatOutputWrapper","dataset","block_ai_chatComponent","scrollTop","scrollHeight","containerHeight","clientHeight","offsetHeight","previousMessage","previousElementSibling","promptVisibleHeight","parseFloat","getComputedStyle","lineHeight","scrollPosition","offsetTop","max","requestAnimationFrame","focus","style","height","computedStyles","paddingTop","paddingBottom","borderTop","borderTopWidth","borderBottom","borderBottomWidth","maxHeight","newHeight","min","aiConfig","availability","available","errormessage","purposes","agentAnswer","JSON","parse","chatOutputIntroObject","chatoutput","filter","object","type","chatOutputOutroObject","suggestionContext","intro","suggestions","outro","forEach","formElement","htmlElement","getElementById","newValue","displayValue","suggestiondisplayvalue","plainText","isLong","push","fieldname","label","explanation","elementId","suggestionvalue","suggestionpreview","substring","disabledButtons"],"mappings":"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"} \ No newline at end of file diff --git a/amd/src/components/chat.js b/amd/src/components/chat.js index 27b97d2..257c247 100644 --- a/amd/src/components/chat.js +++ b/amd/src/components/chat.js @@ -378,7 +378,7 @@ class Chat extends BaseContent { if (aiConfig.availability.available === 'disabled') { return aiConfig.availability.errormessage; } - if (aiConfig.purposes[0].available === 'disabled') { + if (aiConfig.purposes[0].available === 'disabled' || aiConfig.purposes[0].available === 'hidden') { return aiConfig.purposes[0].errormessage; } return '';