/// /// /// /// // namespace namespace cf { // interface export interface IChatResponseOptions extends IBasicElementOptions{ response: string; image: string; list: ChatList; isRobotResponse: boolean; tag: ITag; container: HTMLElement; } export const ChatResponseEvents = { USER_ANSWER_CLICKED: "cf-on-user-answer-clicked" } // class export class ChatResponse extends BasicElement { public static list: ChatList; private static THINKING_MARKUP: string = "

...

"; public isRobotResponse: boolean; public response: string; public originalResponse: string; // keep track of original response with id pipings public parsedResponse: string; private uiOptions: IUserInterfaceOptions; private textEl: Element; private image: string; private container: HTMLElement; private _tag: ITag; private readyTimer: any; private responseLink: ChatResponse; // robot reference from use private onReadyCallback: () => void; private onClickCallback: () => void; public get tag(): ITag{ return this._tag; } public get added() : boolean { return !!this.el || !!this.el.parentNode || !!this.el.parentNode.parentNode; } public get disabled() : boolean { return this.el.classList.contains("disabled"); } public set disabled(value : boolean) { if(value) this.el.classList.add("disabled"); else this.el.classList.remove("disabled"); } /** * We depend on scroll in a column-reverse flex container. This is where Edge and Firefox comes up short */ private hasFlexBug():boolean { return this.cfReference.el.classList.contains('browser-firefox') || this.cfReference.el.classList.contains('browser-edge'); } private animateIn() { const outer:HTMLElement = document.querySelector('scrollable'); const inner:HTMLElement = document.querySelector('.scrollableInner'); if (this.hasFlexBug()) inner.classList.remove('scroll'); requestAnimationFrame(() => { var height = this.el.scrollHeight; this.el.style.height = '0px'; requestAnimationFrame(() => { this.el.style.height = height + 'px'; this.el.classList.add('show'); // Listen for transitionend and set to height:auto try { const sm = window.getComputedStyle(document.querySelectorAll('p.show')[0]); const cssAnimationTime: number = +sm.animationDuration.replace('s', ''); // format '0.234234xs const cssAnimationDelayTime: number = +sm.animationDelay.replace('s', ''); setTimeout(() => { this.el.style.height = 'auto'; if (this.hasFlexBug() && inner.scrollHeight > outer.offsetHeight) { inner.classList.add('scroll'); inner.scrollTop = inner.scrollHeight; } }, (cssAnimationTime + cssAnimationDelayTime) * 1500); } catch(err) { // Fallback method. Assuming animations do not take longer than 1000ms setTimeout(() => { if (this.hasFlexBug() && inner.scrollHeight > outer.offsetHeight) { inner.classList.add('scroll'); inner.scrollTop = inner.scrollHeight; } this.el.style.height = 'auto'; }, 3000); } }); }); } public set visible(value: boolean){ } public get strippedSesponse():string{ var html = this.response; // use browsers native way of stripping var div = document.createElement("div"); div.innerHTML = html; return div.textContent || div.innerText || ""; } constructor(options: IChatResponseOptions){ super(options); this.container = options.container; this.uiOptions = options.cfReference.uiOptions; this._tag = options.tag; } public whenReady(resolve: () => void){ this.onReadyCallback = resolve; } public setValue(dto: FlowDTO = null){ // if(!this.visible){ // this.visible = true; // } const isThinking: boolean = this.el.hasAttribute("thinking"); if(!dto){ this.setToThinking(); }else{ // same same this.response = this.originalResponse = dto.text; this.processResponseAndSetText(); if(this.responseLink && !this.isRobotResponse){ // call robot and update for binding values -> this.responseLink.processResponseAndSetText(); } // check for if response type is file upload... if(dto && dto.controlElements && dto.controlElements[0]){ switch(dto.controlElements[0].type){ case "UploadFileUI" : this.textEl.classList.add("file-icon"); break; } } if(!this.isRobotResponse && !this.onClickCallback){ // edit this.onClickCallback = this.onClick.bind(this); this.el.addEventListener(Helpers.getMouseEvent("click"), this.onClickCallback, false); } } } public show(){ this.visible = true; this.disabled = false; if(!this.response){ this.setToThinking(); }else{ this.checkForEditMode(); } } public updateThumbnail(src: string){ const thumbEl: HTMLElement = this.el.getElementsByTagName("thumb")[0]; if(src.indexOf("text:") === 0){ const thumbElSpan: HTMLElement = thumbEl.getElementsByTagName("span")[0]; thumbElSpan.innerHTML = src.split("text:")[1]; thumbElSpan.setAttribute("length", src.length.toString()); } else { this.image = src; thumbEl.style.backgroundImage = 'url("' + this.image + '")'; } } public setLinkToOtherReponse(response: ChatResponse){ // link reponse to another one, keeping the update circle complete. this.responseLink = response; } public processResponseAndSetText(){ if(!this.originalResponse) return; var innerResponse: string = this.originalResponse; if(this._tag && this._tag.type == "password" && !this.isRobotResponse){ var newStr: string = ""; for (let i = 0; i < innerResponse.length; i++) { newStr += "*"; } innerResponse = newStr; } // if robot, then check linked response for binding values if(this.responseLink && this.isRobotResponse){ // one way data binding values: innerResponse = innerResponse.split("{previous-answer}").join(this.responseLink.parsedResponse); } if(this.isRobotResponse){ // Piping, look through IDs, and map values to dynamics const reponses: Array = ChatResponse.list.getResponses(); for (var i = 0; i < reponses.length; i++) { var response: ChatResponse = reponses[i]; if(response !== this){ if(response.tag){ // check for id, standard if(response.tag.id){ innerResponse = innerResponse.split("{" + response.tag.id + "}").join( response.tag.value); } //fallback check for name if(response.tag.name){ innerResponse = innerResponse.split("{" + response.tag.name + "}").join( response.tag.value); } } } } } // check if response contains an image as answer const responseContains: boolean = innerResponse.indexOf("contains-image") != -1; if(responseContains) this.textEl.classList.add("contains-image"); // now set it if(this.isRobotResponse){ this.textEl.innerHTML = ""; if(!this.uiOptions) this.uiOptions = this.cfReference.uiOptions; // On edit uiOptions are empty, so this mitigates the problem. Not ideal. let robotInitResponseTime: number = this.uiOptions.robot.robotResponseTime; if (robotInitResponseTime != 0){ this.setToThinking(); } // robot response, allow for && for multiple responses var chainedResponses: Array = innerResponse.split("&&"); if(robotInitResponseTime === 0){ for (let i = 0; i < chainedResponses.length; i++) { let str: string = chainedResponses[i]; this.textEl.innerHTML += "

" + str + "

"; } for (let i = 0; i < chainedResponses.length; i++) { setTimeout(() =>{ this.tryClearThinking(); const p: NodeListOf = this.textEl.getElementsByTagName("p"); p[i].classList.add("show"); this.scrollTo(); },chainedResponses.length > 1 && i > 0 ? robotInitResponseTime + ((i + 1) * this.uiOptions.robot.chainedResponseTime) : 0); } } else { for (let i = 0; i < chainedResponses.length; i++) { const revealAfter = robotInitResponseTime + (i * this.uiOptions.robot.chainedResponseTime); let str: string = chainedResponses[i]; setTimeout(() =>{ this.tryClearThinking(); this.textEl.innerHTML += "

" + str + "

"; const p: NodeListOf = this.textEl.getElementsByTagName("p"); p[i].classList.add("show"); this.scrollTo(); }, revealAfter); } } this.readyTimer = setTimeout(() => { if(this.onReadyCallback) this.onReadyCallback(); // reset, as it can be called again this.onReadyCallback = null; if(this._tag && this._tag.skipUserInput === true){ setTimeout(() =>{ this._tag.flowManager.nextStep() this._tag.skipUserInput = false; // to avoid nextStep being fired again as this would make the flow jump too far when editing a response },this.uiOptions.robot.chainedResponseTime); } }, robotInitResponseTime + (chainedResponses.length * this.uiOptions.robot.chainedResponseTime)); } else { // user response, act normal this.tryClearThinking(); const hasImage = innerResponse.indexOf(' -1; const imageRegex = new RegExp(']*?>', 'g'); const imageTag = innerResponse.match(imageRegex); if (hasImage && imageTag) { innerResponse = innerResponse.replace(imageTag[0], ''); this.textEl.innerHTML = `

${imageTag}${innerResponse}

`; } else { this.textEl.innerHTML = `

${innerResponse}

`; } const p: NodeListOf = this.textEl.getElementsByTagName("p"); p[p.length - 1].offsetWidth; p[p.length - 1].classList.add("show"); this.scrollTo(); } this.parsedResponse = innerResponse; // } // value set, so add element, if not added if ( this.uiOptions.robot && this.uiOptions.robot.robotResponseTime === 0 ) { this.addSelf(); } else { setTimeout(() => { this.addSelf(); }, 0); } // bounce this.textEl.removeAttribute("value-added"); setTimeout(() => { this.textEl.setAttribute("value-added", ""); this.el.classList.add("peak-thumb"); }, 0); this.checkForEditMode(); // update response // remove the double ampersands if present this.response = innerResponse.split("&&").join(" "); } public scrollTo(){ const y: number = this.el.offsetTop; const h: number = this.el.offsetHeight; if(!this.container && this.el) this.container = this.el; // On edit this.container is empty so this is a fix to reassign it. Not ideal, but... if ( this.container && this.container.parentElement && this.container.parentElement.scrollHeight ) { this.container.parentElement.scrollTop = y + h + this.container.parentElement.scrollHeight; } } private checkForEditMode(){ if(!this.isRobotResponse && !this.el.hasAttribute("thinking")){ this.el.classList.add("can-edit"); this.disabled = false; } } private tryClearThinking(){ if(this.el.hasAttribute("thinking")){ this.textEl.innerHTML = ""; this.el.removeAttribute("thinking"); } } private setToThinking(){ const canShowThinking: boolean = (this.isRobotResponse && this.uiOptions.robot.robotResponseTime !== 0) || (!this.isRobotResponse && this.cfReference.uiOptions.user.showThinking && !this._tag.skipUserInput); if(canShowThinking){ this.textEl.innerHTML = ChatResponse.THINKING_MARKUP; this.el.classList.remove("can-edit"); this.el.setAttribute("thinking", ""); } if(this.cfReference.uiOptions.user.showThinking || this.cfReference.uiOptions.user.showThumb){ this.addSelf(); } } /** * @name addSelf * add one self to the chat list */ private addSelf(): void { if(this.el.parentNode != this.container){ this.container.appendChild(this.el); this.animateIn(); } } /** * @name onClickCallback * click handler for el */ private onClick(event: MouseEvent): void { this.setToThinking(); ConversationalForm.illustrateFlow(this, "dispatch", ChatResponseEvents.USER_ANSWER_CLICKED, event); this.eventTarget.dispatchEvent(new CustomEvent(ChatResponseEvents.USER_ANSWER_CLICKED, { detail: this._tag })); } protected setData(options: IChatResponseOptions):void{ this.image = options.image; this.response = this.originalResponse = options.response; this.isRobotResponse = options.isRobotResponse; super.setData(options); } protected onElementCreated(){ this.textEl = this.el.getElementsByTagName("text")[0]; this.updateThumbnail(this.image); if(this.isRobotResponse || this.response != null){ // Robot is pseudo thinking, can also be user --> // , but if addUserChatResponse is called from ConversationalForm, then the value is there, therefore skip ... setTimeout(() =>{ this.setValue({text: this.response}) }, 0); //ConversationalForm.animationsEnabled ? Helpers.lerp(Math.random(), 500, 900) : 0); }else{ if(this.cfReference.uiOptions.user.showThumb){ this.el.classList.add("peak-thumb"); } } } public dealloc(){ clearTimeout(this.readyTimer); this.container = null; this.uiOptions = null; this.onReadyCallback = null; if(this.onClickCallback){ this.el.removeEventListener(Helpers.getMouseEvent("click"), this.onClickCallback, false); this.onClickCallback = null; } super.dealloc(); } // template, can be overwritten ... public getTemplate () : string { return ` `; } } }