Add massive collection of CSS, JavaScript and theme assets that were previously excluded: **CSS Files (681 total):** - HVAC plugin-specific styles (hvac-*.css): 34 files including dashboard, certificates, registration, mobile nav, accessibility fixes, animations, and welcome popup - Theme framework files (Astra, builder systems, layouts): 200+ files - Plugin compatibility styles (WooCommerce, WPForms, Elementor, Contact Form 7): 150+ files - WordPress core and editor styles: 50+ files - Responsive and RTL language support: 200+ files **JavaScript Files (400+ total):** - HVAC plugin functionality (hvac-*.js): 27 files including menu systems, dashboard enhancements, profile sharing, mobile responsive features, accessibility, and animations - Framework and library files: jQuery plugins, GSAP, AOS, Swiper, Chart.js, Lottie, Isotope - Plugin compatibility scripts: WPForms, WooCommerce, Elementor, Contact Form 7, LifterLMS - WordPress core functionality: customizer, admin, block editor compatibility - Third-party integrations: Stripe, SMTP, analytics, search functionality **Assets:** - Certificate background images and logos - Comprehensive theme styling infrastructure - Mobile-responsive design systems - Cross-browser compatibility assets - Performance-optimized minified versions **Updated .gitignore:** - Fixed asset directory whitelisting patterns to properly include CSS/JS/images - Added proper directory structure recognition (!/assets/css/, !/assets/js/, etc.) - Maintains security by excluding sensitive files while including essential assets This commit provides the complete frontend infrastructure needed for: - Full theme functionality and styling - Plugin feature implementations - Mobile responsiveness and accessibility - Cross-browser compatibility - Performance optimization - Developer workflow support
		
			
				
	
	
		
			1247 lines
		
	
	
	
		
			30 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			1247 lines
		
	
	
	
		
			30 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* global wpforms_ai_chat_element, WPFormsAIModal, wpf */
 | |
| 
 | |
| /**
 | |
|  * @param this.modeStrings.learnMore
 | |
|  * @param wpforms_ai_chat_element.dislike
 | |
|  * @param wpforms_ai_chat_element.refresh
 | |
|  * @param wpforms_ai_chat_element.confirm.refreshTitle
 | |
|  * @param wpforms_ai_chat_element.confirm.refreshMessage
 | |
|  * @param this.modeStrings.samplePrompts
 | |
|  * @param this.modeStrings.errors.rate_limit
 | |
|  * @param this.modeStrings.reasons.rate_limit
 | |
|  * @param this.modeStrings.descrEndDot
 | |
|  */
 | |
| 
 | |
| /**
 | |
|  * The `WPFormsAIChatHTMLElement` element loader.
 | |
|  *
 | |
|  * @since 1.9.2
 | |
|  */
 | |
| ( function() {
 | |
| 	// Additional modules provided by wpforms_ai_chat_element.
 | |
| 	const modules = wpforms_ai_chat_element.modules || [];
 | |
| 
 | |
| 	// Import all modules dynamically.
 | |
| 	Promise.all( modules.map( ( module ) => import( module.path ) ) )
 | |
| 		.then( ( importedModules ) => {
 | |
| 			// Create the helpers object dynamically.
 | |
| 			const helpers = {};
 | |
| 			let api;
 | |
| 
 | |
| 			importedModules.forEach( ( module, index ) => {
 | |
| 				const moduleName = modules[ index ].name;
 | |
| 				if ( moduleName === 'api' ) {
 | |
| 					api = module.default();
 | |
| 
 | |
| 					return;
 | |
| 				}
 | |
| 				helpers[ moduleName ] = module.default;
 | |
| 			} );
 | |
| 
 | |
| 			window.WPFormsAi = {
 | |
| 				api,
 | |
| 				helpers,
 | |
| 			};
 | |
| 
 | |
| 			// Register the custom HTML element.
 | |
| 			customElements.define( 'wpforms-ai-chat', WPFormsAIChatHTMLElement ); // eslint-disable-line no-use-before-define
 | |
| 		} )
 | |
| 		.catch( ( error ) => {
 | |
| 			wpf.debug( 'Error importing modules:', error );
 | |
| 		} );
 | |
| }() );
 | |
| 
 | |
| /**
 | |
|  * The WPForms AI chat.
 | |
|  *
 | |
|  * Custom HTML element class.
 | |
|  *
 | |
|  * @since 1.9.1
 | |
|  */
 | |
| class WPFormsAIChatHTMLElement extends HTMLElement {
 | |
| 	/**
 | |
| 	 * Element constructor.
 | |
| 	 *
 | |
| 	 * @since 1.9.1
 | |
| 	 */
 | |
| 	constructor() { // eslint-disable-line no-useless-constructor
 | |
| 		// Always call super first in constructor.
 | |
| 		super();
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Element connected to the DOM.
 | |
| 	 *
 | |
| 	 * @since 1.9.1
 | |
| 	 */
 | |
| 	connectedCallback() { // eslint-disable-line complexity
 | |
| 		// Init chat properties.
 | |
| 		this.chatMode = this.getAttribute( 'mode' ) ?? 'text';
 | |
| 		this.fieldId = this.getAttribute( 'field-id' ) ?? '';
 | |
| 		this.prefill = this.getAttribute( 'prefill' ) ?? '';
 | |
| 		this.autoSubmit = this.getAttribute( 'auto-submit' ) === 'true';
 | |
| 		this.modeStrings = wpforms_ai_chat_element[ this.chatMode ] ?? {};
 | |
| 		this.loadingState = false;
 | |
| 
 | |
| 		// Init chat helpers according to the chat mode.
 | |
| 		this.modeHelpers = this.getHelpers( this );
 | |
| 
 | |
| 		// Bail if chat mode helpers not found.
 | |
| 		if ( ! this.modeHelpers ) {
 | |
| 			console.error( `WPFormsAI error: chat mode "${ this.chatMode }" helpers not found` ); // eslint-disable-line no-console
 | |
| 
 | |
| 			return;
 | |
| 		}
 | |
| 
 | |
| 		// Render chat HTML.
 | |
| 		if ( ! this.innerHTML.trim() ) {
 | |
| 			this.innerHTML = this.getInnerHTML();
 | |
| 		}
 | |
| 
 | |
| 		// Get chat elements.
 | |
| 		this.wrapper = this.querySelector( '.wpforms-ai-chat' );
 | |
| 		this.input = this.querySelector( '.wpforms-ai-chat-message-input input, .wpforms-ai-chat-message-input textarea' );
 | |
| 		this.welcomeScreenSamplePrompts = this.querySelector( '.wpforms-ai-chat-welcome-screen-sample-prompts' );
 | |
| 		this.sendButton = this.querySelector( '.wpforms-ai-chat-send' );
 | |
| 		this.stopButton = this.querySelector( '.wpforms-ai-chat-stop' );
 | |
| 		this.messageList = this.querySelector( '.wpforms-ai-chat-message-list' );
 | |
| 
 | |
| 		// Flags.
 | |
| 		this.isTextarea = this.input.tagName === 'TEXTAREA';
 | |
| 		this.preventResizeInput = false;
 | |
| 
 | |
| 		// Compact scrollbar for non-Mac devices.
 | |
| 		if ( ! navigator.userAgent.includes( 'Macintosh' ) ) {
 | |
| 			this.messageList.classList.add( 'wpforms-scrollbar-compact' );
 | |
| 		}
 | |
| 
 | |
| 		// Bind events.
 | |
| 		this.events();
 | |
| 
 | |
| 		// Init answers.
 | |
| 		this.initAnswers();
 | |
| 
 | |
| 		// Init mode.
 | |
| 		if ( typeof this.modeHelpers.init === 'function' ) {
 | |
| 			this.modeHelpers.init();
 | |
| 		}
 | |
| 
 | |
| 		// Auto-submit if enabled and prefill is provided
 | |
| 		if ( this.autoSubmit && this.prefill ) {
 | |
| 			this.input.value = this.prefill;
 | |
| 
 | |
| 			setTimeout( () => this.sendMessage( true ), 250 );
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Get initial innerHTML markup.
 | |
| 	 *
 | |
| 	 * @since 1.9.1
 | |
| 	 *
 | |
| 	 * @return {string} The inner HTML markup.
 | |
| 	 */
 | |
| 	getInnerHTML() {
 | |
| 		if ( this.modeStrings.chatHtml ) {
 | |
| 			return this.decodeHTMLEntities( this.modeStrings.chatHtml );
 | |
| 		}
 | |
| 
 | |
| 		return `
 | |
| 			<div class="wpforms-ai-chat">
 | |
| 				<div class="wpforms-ai-chat-message-list">
 | |
| 					${ this.getWelcomeScreen() }
 | |
| 				</div>
 | |
| 				<div class="wpforms-ai-chat-message-input">
 | |
| 					${ this.getMessageInputField() }
 | |
| 					<button type="button" class="wpforms-ai-chat-send"></button>
 | |
| 					<button type="button" class="wpforms-ai-chat-stop wpforms-hidden"></button>
 | |
| 				</div>
 | |
| 			</div>
 | |
| 		`;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Get the message input field HTML.
 | |
| 	 *
 | |
| 	 * @since 1.9.2
 | |
| 	 *
 | |
| 	 * @return {string} The message input field markup.
 | |
| 	 */
 | |
| 	getMessageInputField() {
 | |
| 		if ( typeof this.modeHelpers.getMessageInputField === 'function' ) {
 | |
| 			return this.modeHelpers.getMessageInputField();
 | |
| 		}
 | |
| 
 | |
| 		return `<input type="text" placeholder="${ this.modeStrings.placeholder }">`;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Get the Welcome screen HTML markup.
 | |
| 	 *
 | |
| 	 * @since 1.9.1
 | |
| 	 *
 | |
| 	 * @return {string} The Welcome screen markup.
 | |
| 	 */
 | |
| 	getWelcomeScreen() {
 | |
| 		let content;
 | |
| 
 | |
| 		if ( this.modeHelpers.isWelcomeScreen() ) {
 | |
| 			content = this.getWelcomeScreenContent();
 | |
| 		} else {
 | |
| 			this.messagePreAdded = true;
 | |
| 			content = this.modeHelpers.getWarningMessage();
 | |
| 		}
 | |
| 
 | |
| 		return `
 | |
| 			<div class="wpforms-ai-chat-message-item item-primary">
 | |
| 				<div class="wpforms-ai-chat-welcome-screen">
 | |
| 					<div class="wpforms-ai-chat-header">
 | |
| 						<h3 class="wpforms-ai-chat-header-title">${ this.modeStrings.title }</h3>
 | |
| 						<span class="wpforms-ai-chat-header-description">${ this.modeStrings.description }
 | |
| 							<a href="${ this.modeStrings.learnMoreUrl }" target="_blank" rel="noopener noreferrer">${ this.modeStrings.learnMore }</a>${ this.modeStrings.descrEndDot }
 | |
| 						</span>
 | |
| 					</div>
 | |
| 					${ content }
 | |
| 				</div>
 | |
| 			</div>
 | |
| 		`;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Get the welcome screen content.
 | |
| 	 *
 | |
| 	 * @since 1.9.4
 | |
| 	 *
 | |
| 	 * @return {string} The welcome screen content.
 | |
| 	 */
 | |
| 	getWelcomeScreenContent() {
 | |
| 		const samplePrompts = this.modeStrings?.samplePrompts,
 | |
| 			li = [];
 | |
| 
 | |
| 		if ( ! samplePrompts && ! this.modeStrings?.initialChat ) {
 | |
| 			return '';
 | |
| 		}
 | |
| 
 | |
| 		if ( samplePrompts ) {
 | |
| 			// Render sample prompts.
 | |
| 			for ( const i in samplePrompts ) {
 | |
| 				li.push( `
 | |
| 					<li>
 | |
| 						<i class="${ samplePrompts[ i ].icon }"></i>
 | |
| 						<a href="#">${ samplePrompts[ i ].title }</a>
 | |
| 					</li>
 | |
| 				` );
 | |
| 			}
 | |
| 
 | |
| 			return `
 | |
| 				<ul class="wpforms-ai-chat-welcome-screen-sample-prompts">
 | |
| 					${ li.join( '' ) }
 | |
| 				</ul>
 | |
| 			`;
 | |
| 		}
 | |
| 
 | |
| 		if ( this.prefill.length > 0 ) {
 | |
| 			return '';
 | |
| 		}
 | |
| 
 | |
| 		this.messagePreAdded = true;
 | |
| 
 | |
| 		return this.modeHelpers?.getInitialChat( this.modeStrings.initialChat );
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Get the spinner SVG image.
 | |
| 	 *
 | |
| 	 * @since 1.9.1
 | |
| 	 *
 | |
| 	 * @return {string} The spinner SVG markup.
 | |
| 	 */
 | |
| 	getSpinnerSvg() {
 | |
| 		return `<svg class="wpforms-ai-chat-spinner-dots" width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><style>.spinner_S1WN{animation:spinner_MGfb .8s linear infinite;animation-delay:-.8s; fill: currentColor;}.spinner_Km9P{animation-delay:-.65s}.spinner_JApP{animation-delay:-.5s}@keyframes spinner_MGfb{93.75%,100%{opacity:.2}}</style><circle class="spinner_S1WN" cx="4" cy="12" r="3"/><circle class="spinner_S1WN spinner_Km9P" cx="12" cy="12" r="3"/><circle class="spinner_S1WN spinner_JApP" cx="20" cy="12" r="3"/></svg>`;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Add event listeners.
 | |
| 	 *
 | |
| 	 * @since 1.9.1
 | |
| 	 */
 | |
| 	events() {
 | |
| 		this.sendButton.addEventListener( 'click', this.sendMessage.bind( this ) );
 | |
| 		this.stopButton.addEventListener( 'click', this.stopLoading.bind( this ) );
 | |
| 		this.input.addEventListener( 'keyup', this.keyUp.bind( this ) );
 | |
| 		this.bindWelcomeScreenEvents();
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Bind welcome screen events.
 | |
| 	 *
 | |
| 	 * @since 1.9.1
 | |
| 	 */
 | |
| 	bindWelcomeScreenEvents() {
 | |
| 		if ( this.welcomeScreenSamplePrompts === null ) {
 | |
| 			return;
 | |
| 		}
 | |
| 
 | |
| 		// Click on the default item in the welcome screen.
 | |
| 		this.welcomeScreenSamplePrompts.querySelectorAll( 'li' ).forEach( ( li ) => {
 | |
| 			li.addEventListener( 'click', this.clickDefaultItem.bind( this ) );
 | |
| 
 | |
| 			li.addEventListener( 'keydown', ( e ) => {
 | |
| 				if ( e.code === 'Enter' ) {
 | |
| 					e.preventDefault();
 | |
| 					this.clickDefaultItem( e );
 | |
| 				}
 | |
| 			} );
 | |
| 		} );
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Init all answers.
 | |
| 	 *
 | |
| 	 * @since 1.9.2
 | |
| 	 */
 | |
| 	initAnswers() {
 | |
| 		if ( ! this.modeStrings.chatHtml ) {
 | |
| 			return;
 | |
| 		}
 | |
| 
 | |
| 		this.wpformsAiApi = this.getAiApi();
 | |
| 
 | |
| 		this.messageList.querySelectorAll( '.wpforms-chat-item-answer' ).forEach( ( answer ) => {
 | |
| 			this.initAnswer( answer );
 | |
| 		} );
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Keyboard `keyUp` event handler.
 | |
| 	 *
 | |
| 	 * @since 1.9.1
 | |
| 	 *
 | |
| 	 * @param {KeyboardEvent} e The keyboard event.
 | |
| 	 */
 | |
| 	keyUp( e ) { // eslint-disable-line complexity
 | |
| 		switch ( e.code ) {
 | |
| 			case 'Enter':
 | |
| 				// Send a message on `Enter` key press.
 | |
| 				// In the case of textarea, `Shift + Enter` adds a new line.
 | |
| 				if ( ! this.isTextarea || ( this.isTextarea && ! e.shiftKey ) ) {
 | |
| 					e.preventDefault();
 | |
| 					this.sendMessage();
 | |
| 				}
 | |
| 
 | |
| 				break;
 | |
| 
 | |
| 			case 'ArrowUp':
 | |
| 				// Navigate through the chat history.
 | |
| 				// In the case of textarea, `Ctrl + Up` is used.
 | |
| 				if ( ! this.isTextarea || ( this.isTextarea && e.ctrlKey ) ) {
 | |
| 					e.preventDefault();
 | |
| 					this.arrowUp();
 | |
| 				}
 | |
| 				break;
 | |
| 
 | |
| 			case 'ArrowDown':
 | |
| 				// Navigate through the chat history.
 | |
| 				// In the case of textarea, `Ctrl + Down` is used.
 | |
| 				if ( ! this.isTextarea || ( this.isTextarea && e.ctrlKey ) ) {
 | |
| 					e.preventDefault();
 | |
| 					this.arrowDown();
 | |
| 				}
 | |
| 				break;
 | |
| 
 | |
| 			default:
 | |
| 				// Update the chat history.
 | |
| 				this.history.update( { question: this.input.value } );
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Send a question message to the chat.
 | |
| 	 *
 | |
| 	 * @since 1.9.1
 | |
| 	 *
 | |
| 	 * @param {boolean} allowHTML Whether to allow HTML in the message.
 | |
| 	 */
 | |
| 	sendMessage( allowHTML = false ) {
 | |
| 		let message = this.input.value;
 | |
| 
 | |
| 		if ( ! message ) {
 | |
| 			return;
 | |
| 		}
 | |
| 
 | |
| 		if ( ! allowHTML ) {
 | |
| 			message = this.htmlSpecialChars( message );
 | |
| 		}
 | |
| 
 | |
| 		// Fire event before sending the message.
 | |
| 		this.triggerEvent( 'wpformsAIChatBeforeSendMessage', { fieldId: this.fieldId, mode: this.chatMode } );
 | |
| 
 | |
| 		this.addFirstMessagePre();
 | |
| 		this.welcomeScreenSamplePrompts?.remove();
 | |
| 
 | |
| 		this.resetInput();
 | |
| 		this.addMessage( message, true );
 | |
| 		this.startLoading();
 | |
| 
 | |
| 		if ( message.trim() === '' ) {
 | |
| 			this.addEmptyResultsError();
 | |
| 
 | |
| 			return;
 | |
| 		}
 | |
| 
 | |
| 		if ( typeof this.modeHelpers.prepareMessage === 'function' ) {
 | |
| 			message = this.modeHelpers.prepareMessage( message );
 | |
| 		}
 | |
| 
 | |
| 		this.getAiApi()
 | |
| 			.prompt( message, this.sessionId )
 | |
| 			.then( this.addAnswer.bind( this ) )
 | |
| 			.catch( this.apiResponseError.bind( this ) );
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * AI API error handler.
 | |
| 	 *
 | |
| 	 * @since 1.9.2
 | |
| 	 *
 | |
| 	 * @param {Object|string} error The error object or string.
 | |
| 	 */
 | |
| 	apiResponseError( error ) { // eslint-disable-line complexity
 | |
| 		const cause	= error?.cause ?? null;
 | |
| 
 | |
| 		// Handle the rate limit error.
 | |
| 		if ( cause === 429 ) {
 | |
| 			this.addError(
 | |
| 				this.modeStrings.errors.rate_limit || wpforms_ai_chat_element.errors.rate_limit,
 | |
| 				this.modeStrings.reasons.rate_limit || wpforms_ai_chat_element.reasons.rate_limit
 | |
| 			);
 | |
| 
 | |
| 			return;
 | |
| 		}
 | |
| 
 | |
| 		// Handle the Internal Server Error.
 | |
| 		if ( cause === 500 ) {
 | |
| 			this.addEmptyResultsError();
 | |
| 
 | |
| 			return;
 | |
| 		}
 | |
| 
 | |
| 		this.addError(
 | |
| 			error.message || this.modeStrings.errors.default || wpforms_ai_chat_element.errors.default,
 | |
| 			this.modeStrings.reasons.default || wpforms_ai_chat_element.reasons.default
 | |
| 		);
 | |
| 
 | |
| 		wpf.debug( 'WPFormsAI error: ', error );
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Before the first message.
 | |
| 	 *
 | |
| 	 * @since 1.9.1
 | |
| 	 */
 | |
| 	addFirstMessagePre() {
 | |
| 		if ( this.sessionId || this.messagePreAdded ) {
 | |
| 			return;
 | |
| 		}
 | |
| 
 | |
| 		this.messagePreAdded = true;
 | |
| 
 | |
| 		const divider = document.createElement( 'div' );
 | |
| 
 | |
| 		divider.classList.add( 'wpforms-ai-chat-divider' );
 | |
| 		this.messageList.appendChild( divider );
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Click on the default item in the welcome screen.
 | |
| 	 *
 | |
| 	 * @since 1.9.1
 | |
| 	 *
 | |
| 	 * @param {Event} e The event object.
 | |
| 	 */
 | |
| 	clickDefaultItem( e ) {
 | |
| 		const li = e.target.nodeName === 'LI' ? e.target : e.target.closest( 'li' );
 | |
| 		const message = li.querySelector( 'a' )?.textContent;
 | |
| 
 | |
| 		e.preventDefault();
 | |
| 
 | |
| 		if ( ! message ) {
 | |
| 			return;
 | |
| 		}
 | |
| 
 | |
| 		this.input.value = message;
 | |
| 
 | |
| 		// Update the chat history.
 | |
| 		this.history.push( { question: message } );
 | |
| 
 | |
| 		this.sendMessage();
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Click on the dislike button.
 | |
| 	 *
 | |
| 	 * @since 1.9.1
 | |
| 	 *
 | |
| 	 * @param {Event} e The event object.
 | |
| 	 */
 | |
| 	clickDislikeButton( e ) {
 | |
| 		const button = e.target;
 | |
| 		const answer = button?.closest( '.wpforms-chat-item-answer' );
 | |
| 
 | |
| 		if ( ! answer ) {
 | |
| 			return;
 | |
| 		}
 | |
| 
 | |
| 		button.classList.add( 'clicked' );
 | |
| 		button.setAttribute( 'disabled', true );
 | |
| 
 | |
| 		const responseId = answer.getAttribute( 'data-response-id' );
 | |
| 
 | |
| 		this.wpformsAiApi.rate( false, responseId );
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Click on the refresh button.
 | |
| 	 *
 | |
| 	 * @since 1.9.1
 | |
| 	 */
 | |
| 	async clickRefreshButton() {
 | |
| 		const refreshConfirm = () => {
 | |
| 			// Restore the welcome screen.
 | |
| 			this.prefill = '';
 | |
| 			this.messageList.innerHTML = this.getWelcomeScreen();
 | |
| 			this.welcomeScreenSamplePrompts = this.querySelector( '.wpforms-ai-chat-welcome-screen-sample-prompts' );
 | |
| 			this.bindWelcomeScreenEvents();
 | |
| 			this.scrollMessagesTo( 'top' );
 | |
| 
 | |
| 			// Clear the session ID.
 | |
| 			this.wpformsAiApi = null;
 | |
| 			this.sessionId = null;
 | |
| 			this.messagePreAdded = null;
 | |
| 			this.wrapper.removeAttribute( 'data-session-id' );
 | |
| 
 | |
| 			// Clear the chat history.
 | |
| 			this.history.clear();
 | |
| 
 | |
| 			// Fire the event after refreshing the chat.
 | |
| 			this.triggerEvent( 'wpformsAIChatAfterRefresh', { fieldId: this.fieldId } );
 | |
| 		};
 | |
| 
 | |
| 		const refreshCancel = () => {
 | |
| 			// Fire the event when refresh is canceled.
 | |
| 			this.triggerEvent( 'wpformsAIChatCancelRefresh', { fieldId: this.fieldId } );
 | |
| 		};
 | |
| 
 | |
| 		// Fire the event before refresh confirmation is opened.
 | |
| 		this.triggerEvent( 'wpformsAIChatBeforeRefreshConfirm', { fieldId: this.fieldId } );
 | |
| 
 | |
| 		// Open a confirmation modal.
 | |
| 		WPFormsAIModal.confirmModal( {
 | |
| 			title: wpforms_ai_chat_element.confirm.refreshTitle,
 | |
| 			content: wpforms_ai_chat_element.confirm.refreshMessage,
 | |
| 			onConfirm: refreshConfirm,
 | |
| 			onCancel: refreshCancel,
 | |
| 		} );
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Start loading.
 | |
| 	 *
 | |
| 	 * @since 1.9.1
 | |
| 	 */
 | |
| 	startLoading() {
 | |
| 		this.loadingState = true;
 | |
| 		this.sendButton.classList.add( 'wpforms-hidden' );
 | |
| 		this.stopButton.classList.remove( 'wpforms-hidden' );
 | |
| 		this.input.setAttribute( 'disabled', true );
 | |
| 		this.input.setAttribute( 'placeholder', this.modeStrings.waiting );
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Stop loading.
 | |
| 	 *
 | |
| 	 * @since 1.9.1
 | |
| 	 */
 | |
| 	stopLoading() {
 | |
| 		this.loadingState = false;
 | |
| 		this.messageList.querySelector( '.wpforms-chat-item-answer-waiting' )?.remove();
 | |
| 		this.sendButton.classList.remove( 'wpforms-hidden' );
 | |
| 		this.stopButton.classList.add( 'wpforms-hidden' );
 | |
| 		this.input.removeAttribute( 'disabled' );
 | |
| 		this.input.setAttribute( 'placeholder', this.modeStrings.placeholder );
 | |
| 		this.input.focus();
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Keyboard `ArrowUp` key event handler.
 | |
| 	 *
 | |
| 	 * @since 1.9.1
 | |
| 	 */
 | |
| 	arrowUp() {
 | |
| 		const prev = this.history.prev()?.question;
 | |
| 
 | |
| 		if ( typeof prev !== 'undefined' ) {
 | |
| 			this.input.value = prev;
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Keyboard `ArrowDown` key event handler.
 | |
| 	 *
 | |
| 	 * @since 1.9.1
 | |
| 	 */
 | |
| 	arrowDown() {
 | |
| 		const next = this.history.next()?.question;
 | |
| 
 | |
| 		if ( typeof next !== 'undefined' ) {
 | |
| 			this.input.value = next;
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Get AI API object instance.
 | |
| 	 *
 | |
| 	 * @since 1.9.1
 | |
| 	 *
 | |
| 	 * @return {Object} The AI API object.
 | |
| 	 */
 | |
| 	getAiApi() {
 | |
| 		if ( this.wpformsAiApi ) {
 | |
| 			return this.wpformsAiApi;
 | |
| 		}
 | |
| 
 | |
| 		// Attempt to get the session ID from the element attribute OR the data attribute.
 | |
| 		// It is necessary to restore the session ID after restoring the chat element.
 | |
| 		this.sessionId = this.wrapper.getAttribute( 'data-session-id' ) || null;
 | |
| 
 | |
| 		// Create a new AI API object instance.
 | |
| 		this.wpformsAiApi = window.WPFormsAi.api( this.chatMode, this.sessionId );
 | |
| 
 | |
| 		return this.wpformsAiApi;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Scroll message list to given edge.
 | |
| 	 *
 | |
| 	 * @since 1.9.1
 | |
| 	 *
 | |
| 	 * @param {string} edge The edge to scroll to; `top` or `bottom`.
 | |
| 	 */
 | |
| 	scrollMessagesTo( edge = 'bottom' ) {
 | |
| 		if ( edge === 'top' ) {
 | |
| 			this.messageList.scrollTop = 0;
 | |
| 
 | |
| 			return;
 | |
| 		}
 | |
| 
 | |
| 		if ( this.messageList.scrollHeight - this.messageList.scrollTop < 22 ) {
 | |
| 			return;
 | |
| 		}
 | |
| 
 | |
| 		this.messageList.scrollTop = this.messageList.scrollHeight;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Add a message to the chat.
 | |
| 	 *
 | |
| 	 * @since 1.9.1
 | |
| 	 *
 | |
| 	 * @param {string}  message    The message to add.
 | |
| 	 * @param {boolean} isQuestion Whether it is a question.
 | |
| 	 * @param {Object}  response   The response data, optional.
 | |
| 	 *
 | |
| 	 * @return {HTMLElement} The message element.
 | |
| 	 */
 | |
| 	addMessage( message, isQuestion, response = null ) {
 | |
| 		const { messageList } = this;
 | |
| 		const element = document.createElement( 'div' );
 | |
| 
 | |
| 		element.classList.add( 'wpforms-chat-item' );
 | |
| 		messageList.appendChild( element );
 | |
| 
 | |
| 		if ( isQuestion ) {
 | |
| 			// Add a question.
 | |
| 			element.innerHTML = message;
 | |
| 			element.classList.add( 'wpforms-chat-item-question' );
 | |
| 
 | |
| 			// Add a waiting spinner.
 | |
| 			const spinnerWrapper = document.createElement( 'div' ),
 | |
| 				spinner = document.createElement( 'div' );
 | |
| 
 | |
| 			spinnerWrapper.classList.add( 'wpforms-chat-item-answer-waiting' );
 | |
| 			spinner.classList.add( 'wpforms-chat-item-spinner' );
 | |
| 			spinner.innerHTML = this.getSpinnerSvg();
 | |
| 			spinnerWrapper.appendChild( spinner );
 | |
| 			messageList.appendChild( spinnerWrapper );
 | |
| 
 | |
| 			// Add an empty chat history item.
 | |
| 			this.history.push( {} );
 | |
| 		} else {
 | |
| 			// Add an answer.
 | |
| 			const itemContent = document.createElement( 'div' );
 | |
| 
 | |
| 			itemContent.classList.add( 'wpforms-chat-item-content' );
 | |
| 			element.appendChild( itemContent );
 | |
| 
 | |
| 			// Remove the waiting spinner.
 | |
| 			messageList.querySelector( '.wpforms-chat-item-answer-waiting' )?.remove();
 | |
| 
 | |
| 			// Remove the active class from the previous answer.
 | |
| 			this.messageList.querySelector( '.wpforms-chat-item-answer.active' )?.classList.remove( 'active' );
 | |
| 
 | |
| 			// Update element classes and attributes.
 | |
| 			element.classList.add( 'wpforms-chat-item-answer' );
 | |
| 			element.classList.add( 'active' );
 | |
| 			element.classList.add( 'wpforms-chat-item-typing' );
 | |
| 			element.classList.add( 'wpforms-chat-item-' + this.chatMode );
 | |
| 			element.setAttribute( 'data-response-id', response?.responseId ?? '' );
 | |
| 
 | |
| 			// Update the answer in the chat history.
 | |
| 			this.history.update( { answer: message } );
 | |
| 
 | |
| 			// Type the message with the typewriter effect.
 | |
| 			this.typeText( itemContent, message, this.addedAnswer.bind( this ) );
 | |
| 		}
 | |
| 
 | |
| 		this.scrollMessagesTo( 'bottom' );
 | |
| 
 | |
| 		return element;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Add an error to the chat.
 | |
| 	 *
 | |
| 	 * @since 1.9.1
 | |
| 	 *
 | |
| 	 * @param {string} errorTitle  The error title.
 | |
| 	 * @param {string} errorReason The error title.
 | |
| 	 */
 | |
| 	addError( errorTitle, errorReason ) {
 | |
| 		this.addNotice( errorTitle, errorReason );
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Add a warning to the chat.
 | |
| 	 *
 | |
| 	 * @since 1.9.2
 | |
| 	 *
 | |
| 	 * @param {string} warningTitle  The warning title.
 | |
| 	 * @param {string} warningReason The warning reason.
 | |
| 	 */
 | |
| 	addWarning( warningTitle, warningReason ) {
 | |
| 		this.addNotice( warningTitle, warningReason, 'warning' );
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Add a notice to the chat.
 | |
| 	 *
 | |
| 	 * @since 1.9.2
 | |
| 	 *
 | |
| 	 * @param {string} title  The notice title.
 | |
| 	 * @param {string} reason The notice reason.
 | |
| 	 * @param {string} type   The notice type.
 | |
| 	 */
 | |
| 	addNotice( title, reason, type = 'error' ) {
 | |
| 		let content = ``;
 | |
| 
 | |
| 		// Bail if loading was stopped.
 | |
| 		if ( ! this.loadingState ) {
 | |
| 			return;
 | |
| 		}
 | |
| 
 | |
| 		if ( title ) {
 | |
| 			content += `<h4>${ title }</h4>`;
 | |
| 		}
 | |
| 
 | |
| 		if ( reason ) {
 | |
| 			content += `<span>${ reason }</span>`;
 | |
| 		}
 | |
| 
 | |
| 		const chatItem = document.createElement( 'div' );
 | |
| 		const itemContent = document.createElement( 'div' );
 | |
| 
 | |
| 		chatItem.classList.add( 'wpforms-chat-item' );
 | |
| 		chatItem.classList.add( 'wpforms-chat-item-' + type );
 | |
| 		itemContent.classList.add( 'wpforms-chat-item-content' );
 | |
| 		chatItem.appendChild( itemContent );
 | |
| 
 | |
| 		this.messageList.querySelector( '.wpforms-chat-item-answer-waiting' )?.remove();
 | |
| 		this.messageList.appendChild( chatItem );
 | |
| 
 | |
| 		// Add the error to the chat.
 | |
| 		// Type the message with the typewriter effect.
 | |
| 		this.typeText( itemContent, content, () => {
 | |
| 			this.stopLoading();
 | |
| 		} );
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Add an empty results error to the chat.
 | |
| 	 *
 | |
| 	 * @since 1.9.1
 | |
| 	 */
 | |
| 	addEmptyResultsError() {
 | |
| 		this.addError(
 | |
| 			this.modeStrings.errors.empty || wpforms_ai_chat_element.errors.empty,
 | |
| 			this.modeStrings.reasons.empty || wpforms_ai_chat_element.reasons.empty
 | |
| 		);
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Add a prohibited code warning to the chat.
 | |
| 	 *
 | |
| 	 * @since 1.9.2
 | |
| 	 */
 | |
| 	addProhibitedCodeWarning() {
 | |
| 		this.addWarning(
 | |
| 			this.modeStrings.warnings.prohibited_code || wpforms_ai_chat_element.warnings.prohibited_code,
 | |
| 			this.modeStrings.reasons.prohibited_code || wpforms_ai_chat_element.reasons.prohibited_code
 | |
| 		);
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Add an answer to the chat.
 | |
| 	 *
 | |
| 	 * @since 1.9.1
 | |
| 	 *
 | |
| 	 * @param {Object} response The response data to add.
 | |
| 	 */
 | |
| 	addAnswer( response ) {
 | |
| 		// Bail if loading was stopped.
 | |
| 		if ( ! this.loadingState || ! response ) {
 | |
| 			return;
 | |
| 		}
 | |
| 
 | |
| 		// Output processing time to console if available.
 | |
| 		if ( response.processingData ) {
 | |
| 			wpf.debug( 'WPFormsAI processing data:', response.processingData );
 | |
| 		}
 | |
| 
 | |
| 		// Sanitize response.
 | |
| 		const sanitizedResponse = this.sanitizeResponse( { ...response } );
 | |
| 
 | |
| 		if ( this.hasProhibitedCode( response, sanitizedResponse ) ) {
 | |
| 			this.addProhibitedCodeWarning();
 | |
| 
 | |
| 			return;
 | |
| 		}
 | |
| 
 | |
| 		const answerHTML = this.modeHelpers.getAnswer( sanitizedResponse );
 | |
| 
 | |
| 		if ( ! answerHTML ) {
 | |
| 			this.addEmptyResultsError();
 | |
| 
 | |
| 			return;
 | |
| 		}
 | |
| 
 | |
| 		// Store the session ID from response.
 | |
| 		this.sessionId = response.sessionId;
 | |
| 
 | |
| 		// Set the session ID to the chat wrapper data attribute.
 | |
| 		this.wrapper.setAttribute( 'data-session-id', this.sessionId );
 | |
| 
 | |
| 		// Fire the event before adding the answer to the chat.
 | |
| 		this.triggerEvent( 'wpformsAIChatBeforeAddAnswer', { chat: this, response: sanitizedResponse } );
 | |
| 
 | |
| 		// Add the answer to the chat.
 | |
| 		this.addMessage( answerHTML, false, sanitizedResponse );
 | |
| 
 | |
| 		this.triggerEvent( 'wpformsAIChatAfterAddAnswer', { fieldId: this.fieldId } );
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Check if the response has a prohibited code.
 | |
| 	 *
 | |
| 	 * @since 1.9.2
 | |
| 	 *
 | |
| 	 * @param {Object} response          The response data.
 | |
| 	 * @param {Array}  sanitizedResponse The sanitized response data.
 | |
| 	 *
 | |
| 	 * @return {boolean} Whether the answer has a prohibited code.
 | |
| 	 */
 | |
| 	hasProhibitedCode( response, sanitizedResponse ) {
 | |
| 		if ( typeof this.modeHelpers.hasProhibitedCode === 'function' ) {
 | |
| 			return this.modeHelpers.hasProhibitedCode( response, sanitizedResponse );
 | |
| 		}
 | |
| 
 | |
| 		return false;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Sanitize response.
 | |
| 	 *
 | |
| 	 * @since 1.9.2
 | |
| 	 *
 | |
| 	 * @param {Object} response The response data to sanitize.
 | |
| 	 *
 | |
| 	 * @return {Object} The sanitized response.
 | |
| 	 */
 | |
| 	sanitizeResponse( response ) {
 | |
| 		if ( typeof this.modeHelpers.sanitizeResponse === 'function' ) {
 | |
| 			return this.modeHelpers.sanitizeResponse( response );
 | |
| 		}
 | |
| 
 | |
| 		return response;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * The added answer callback.
 | |
| 	 *
 | |
| 	 * @since 1.9.1
 | |
| 	 *
 | |
| 	 * @param {HTMLElement} element The answer element.
 | |
| 	 */
 | |
| 	addedAnswer( element ) {
 | |
| 		// Add answer buttons when typing is finished.
 | |
| 		element.innerHTML += this.getAnswerButtons();
 | |
| 		element.parentElement.classList.remove( 'wpforms-chat-item-typing' );
 | |
| 
 | |
| 		this.stopLoading();
 | |
| 		this.initAnswer( element );
 | |
| 
 | |
| 		// Added answer callback.
 | |
| 		this.modeHelpers.addedAnswer( element );
 | |
| 
 | |
| 		// Fire the event when the answer added to the chat.
 | |
| 		this.triggerEvent( 'wpformsAIChatAddedAnswer', { chat: this, element } );
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Init answer.
 | |
| 	 *
 | |
| 	 * @since 1.9.2
 | |
| 	 *
 | |
| 	 * @param {HTMLElement} element The answer element.
 | |
| 	 */
 | |
| 	initAnswer( element ) {
 | |
| 		if ( ! element ) {
 | |
| 			return;
 | |
| 		}
 | |
| 
 | |
| 		// Prepare answer buttons and init the tooltips.
 | |
| 		element.querySelectorAll( '.wpforms-help-tooltip' ).forEach( ( icon ) => {
 | |
| 			let title = icon.getAttribute( 'title' );
 | |
| 
 | |
| 			if ( ! title ) {
 | |
| 				title =	icon.classList.contains( 'dislike' ) ? wpforms_ai_chat_element.dislike : '';
 | |
| 				title = icon.classList.contains( 'refresh' ) ? wpforms_ai_chat_element.refresh : title;
 | |
| 
 | |
| 				icon.setAttribute( 'title', title );
 | |
| 			}
 | |
| 
 | |
| 			icon.classList.remove( 'tooltipstered' );
 | |
| 		} );
 | |
| 
 | |
| 		wpf.initTooltips( element );
 | |
| 
 | |
| 		// Add event listeners.
 | |
| 		element.addEventListener( 'click', this.setActiveAnswer.bind( this ) );
 | |
| 
 | |
| 		element.querySelector( '.wpforms-ai-chat-answer-button.dislike' )
 | |
| 			?.addEventListener( 'click', this.clickDislikeButton.bind( this ) );
 | |
| 
 | |
| 		element.querySelector( '.wpforms-ai-chat-answer-button.refresh' )
 | |
| 			?.addEventListener( 'click', this.clickRefreshButton.bind( this ) );
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Set active answer.
 | |
| 	 *
 | |
| 	 * @since 1.9.2
 | |
| 	 *
 | |
| 	 * @param {Event} e The event object.
 | |
| 	 */
 | |
| 	setActiveAnswer( e ) {
 | |
| 		let answer = e.target.closest( '.wpforms-chat-item-answer' );
 | |
| 
 | |
| 		answer = answer || e.target;
 | |
| 
 | |
| 		if ( answer.classList.contains( 'active' ) ) {
 | |
| 			return;
 | |
| 		}
 | |
| 
 | |
| 		this.messageList.querySelector( '.wpforms-chat-item-answer.active' )?.classList.remove( 'active' );
 | |
| 		answer.classList.add( 'active' );
 | |
| 
 | |
| 		const responseId = answer.getAttribute( 'data-response-id' );
 | |
| 
 | |
| 		if ( this.modeHelpers.setActiveAnswer ) {
 | |
| 			this.modeHelpers.setActiveAnswer( answer );
 | |
| 		}
 | |
| 
 | |
| 		// Trigger the event.
 | |
| 		this.triggerEvent( 'wpformsAIChatSetActiveAnswer', { chat: this, responseId } );
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Get the answer buttons HTML markup.
 | |
| 	 *
 | |
| 	 * @since 1.9.1
 | |
| 	 *
 | |
| 	 * @return {string} The answer buttons HTML markup.
 | |
| 	 */
 | |
| 	getAnswerButtons() {
 | |
| 		return `
 | |
| 			<div class="wpforms-ai-chat-answer-buttons">
 | |
| 				${ this.modeHelpers.getAnswerButtonsPre() }
 | |
| 				<div class="wpforms-ai-chat-answer-buttons-response">
 | |
| 					<button type="button" class="wpforms-ai-chat-answer-button dislike wpforms-help-tooltip" data-tooltip-position="top" title="${ wpforms_ai_chat_element.dislike }"></button>
 | |
| 					<button type="button" class="wpforms-ai-chat-answer-button refresh wpforms-help-tooltip" data-tooltip-position="top" title="${ wpforms_ai_chat_element.refresh }">
 | |
| 						<i class="fa fa-trash-o"></i>
 | |
| 					</button>
 | |
| 				</div>
 | |
| 			</div>
 | |
| 		`;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Type text into an element with the typewriter effect.
 | |
| 	 *
 | |
| 	 * @since 1.9.1
 | |
| 	 *
 | |
| 	 * @param {HTMLElement} element          The element to type into.
 | |
| 	 * @param {string}      text             The text to type.
 | |
| 	 * @param {Function}    finishedCallback The callback function to call when typing is finished.
 | |
| 	 */
 | |
| 	typeText( element, text, finishedCallback ) {
 | |
| 		const chunkSize = 5;
 | |
| 		const chat = this;
 | |
| 		let index = 0;
 | |
| 		let content = '';
 | |
| 
 | |
| 		/**
 | |
| 		 * Type single character.
 | |
| 		 *
 | |
| 		 * @since 1.9.1
 | |
| 		 */
 | |
| 		function type() {
 | |
| 			const chunk = text.substring( index, index + chunkSize );
 | |
| 
 | |
| 			content += chunk;
 | |
| 			// Remove broken HTML tag from the end of the string.
 | |
| 			element.innerHTML = content.replace( /<[^>]{0,300}$/g, '' );
 | |
| 			index += chunkSize;
 | |
| 
 | |
| 			if ( index < text.length && chat.loadingState ) {
 | |
| 				// Recursive call to output the next chunk.
 | |
| 				setTimeout( type, 20 );
 | |
| 			} else if ( typeof finishedCallback === 'function' ) {
 | |
| 				// Call the callback function when typing is finished.
 | |
| 				finishedCallback( element );
 | |
| 			}
 | |
| 
 | |
| 			chat.scrollMessagesTo( 'bottom' );
 | |
| 		}
 | |
| 
 | |
| 		type();
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Get the `helpers` object according to the chat mode.
 | |
| 	 *
 | |
| 	 * @since 1.9.1
 | |
| 	 *
 | |
| 	 * @param {WPFormsAIChatHTMLElement} chat Chat element.
 | |
| 	 *
 | |
| 	 * @return {Object} Choices helpers object.
 | |
| 	 */
 | |
| 	getHelpers( chat ) {
 | |
| 		const helpers = window.WPFormsAi.helpers;
 | |
| 
 | |
| 		return helpers[ chat.chatMode ]( chat ) ?? null;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Reset the message input field.
 | |
| 	 *
 | |
| 	 * @since 1.9.2
 | |
| 	 */
 | |
| 	resetInput() {
 | |
| 		this.input.value = '';
 | |
| 
 | |
| 		if ( this.modeHelpers.resetInput ) {
 | |
| 			this.modeHelpers.resetInput();
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Escape HTML special characters.
 | |
| 	 *
 | |
| 	 * @since 1.9.1
 | |
| 	 *
 | |
| 	 * @param {string} html HTML string.
 | |
| 	 *
 | |
| 	 * @return {string} Escaped HTML string.
 | |
| 	 */
 | |
| 	htmlSpecialChars( html ) {
 | |
| 		return html.replace( /[<>]/g, ( x ) => '�' + x.charCodeAt( 0 ) + ';' );
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Decode HTML entities.
 | |
| 	 *
 | |
| 	 * @since 1.9.2
 | |
| 	 *
 | |
| 	 * @param {string} html Encoded HTML string.
 | |
| 	 *
 | |
| 	 * @return {string} Decoded HTML string.
 | |
| 	 */
 | |
| 	decodeHTMLEntities( html ) {
 | |
| 		const txt = document.createElement( 'textarea' );
 | |
| 
 | |
| 		txt.innerHTML = html;
 | |
| 
 | |
| 		return txt.value;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Wrapper to trigger a custom event and return the event object.
 | |
| 	 *
 | |
| 	 * @since 1.9.1
 | |
| 	 *
 | |
| 	 * @param {string} eventName Event name to trigger (custom or native).
 | |
| 	 * @param {Object} args      Trigger arguments.
 | |
| 	 *
 | |
| 	 * @return {Event} Event object.
 | |
| 	 */
 | |
| 	triggerEvent( eventName, args = {} ) {
 | |
| 		const event = new CustomEvent( eventName, { detail: args } );
 | |
| 
 | |
| 		document.dispatchEvent( event );
 | |
| 
 | |
| 		return event;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Chat history object.
 | |
| 	 *
 | |
| 	 * @since 1.9.1
 | |
| 	 */
 | |
| 	history = {
 | |
| 		/**
 | |
| 		 * Chat history data.
 | |
| 		 *
 | |
| 		 * @since 1.9.1
 | |
| 		 *
 | |
| 		 * @type {Array}
 | |
| 		 */
 | |
| 		data: [],
 | |
| 
 | |
| 		/**
 | |
| 		 * Chat history pointer.
 | |
| 		 *
 | |
| 		 * @since 1.9.1
 | |
| 		 *
 | |
| 		 * @type {number}
 | |
| 		 */
 | |
| 		pointer: 0,
 | |
| 
 | |
| 		/**
 | |
| 		 * Default item.
 | |
| 		 *
 | |
| 		 * @since 1.9.1
 | |
| 		 *
 | |
| 		 * @type {Object}
 | |
| 		 */
 | |
| 		defaultItem: {
 | |
| 			question: '',
 | |
| 			answer: null,
 | |
| 		},
 | |
| 
 | |
| 		/**
 | |
| 		 * Get history data by pointer.
 | |
| 		 *
 | |
| 		 * @since 1.9.1
 | |
| 		 *
 | |
| 		 * @param {number|null} pointer The history pointer.
 | |
| 		 *
 | |
| 		 * @return {Object} The history item.
 | |
| 		 */
 | |
| 		get( pointer = null ) {
 | |
| 			if ( pointer ) {
 | |
| 				this.pointer = pointer;
 | |
| 			}
 | |
| 
 | |
| 			if ( this.pointer < 1 ) {
 | |
| 				this.pointer = 0;
 | |
| 			} else if ( this.pointer >= this.data.length ) {
 | |
| 				this.pointer = this.data.length - 1;
 | |
| 			}
 | |
| 
 | |
| 			return this.data[ this.pointer ] ?? {};
 | |
| 		},
 | |
| 
 | |
| 		/**
 | |
| 		 * Get history data by pointer.
 | |
| 		 *
 | |
| 		 * @since 1.9.1
 | |
| 		 *
 | |
| 		 * @return {Object} The history item.
 | |
| 		 */
 | |
| 		prev() {
 | |
| 			this.pointer -= 1;
 | |
| 
 | |
| 			return this.get();
 | |
| 		},
 | |
| 
 | |
| 		/**
 | |
| 		 * Get history data by pointer.
 | |
| 		 *
 | |
| 		 * @since 1.9.1
 | |
| 		 *
 | |
| 		 * @return {Object} The history item.
 | |
| 		 */
 | |
| 		next() {
 | |
| 			this.pointer += 1;
 | |
| 
 | |
| 			return this.get();
 | |
| 		},
 | |
| 
 | |
| 		/**
 | |
| 		 * Push an item to the chat history.
 | |
| 		 *
 | |
| 		 * @since 1.9.1
 | |
| 		 *
 | |
| 		 * @param {Object} item The item to push.
 | |
| 		 *
 | |
| 		 * @return {void}
 | |
| 		 */
 | |
| 		push( item ) {
 | |
| 			if ( item.answer ) {
 | |
| 				this.data[ this.data.length - 1 ].answer = item.answer;
 | |
| 
 | |
| 				return;
 | |
| 			}
 | |
| 
 | |
| 			this.data.push( { ...this.defaultItem, ...item } );
 | |
| 			this.pointer = this.data.length - 1;
 | |
| 		},
 | |
| 
 | |
| 		/**
 | |
| 		 * Update the last history item.
 | |
| 		 *
 | |
| 		 * @since 1.9.1
 | |
| 		 *
 | |
| 		 * @param {Object} item The updated history item.
 | |
| 		 *
 | |
| 		 * @return {void}
 | |
| 		 */
 | |
| 		update( item ) {
 | |
| 			const lastKey = this.data.length > 0 ? this.data.length - 1 : 0;
 | |
| 			const lastItem = this.data[ lastKey ] ?? this.defaultItem;
 | |
| 
 | |
| 			this.pointer = lastKey;
 | |
| 			this.data[ lastKey ] = { ...lastItem, ...item };
 | |
| 		},
 | |
| 
 | |
| 		/**
 | |
| 		 * Clear the chat history.
 | |
| 		 *
 | |
| 		 * @since 1.9.1
 | |
| 		 */
 | |
| 		clear() {
 | |
| 			this.data = [];
 | |
| 			this.pointer = 0;
 | |
| 		},
 | |
| 	};
 | |
| }
 |