class AIChatChat extends HTMLElement { #root = null; #input = null; #output = null; #progress = null; #history = []; constructor() { super(); this.#root = this.attachShadow({mode: 'open'}); this.#root.innerHTML = `
`; this.#root.appendChild(this.getStyle()); this.#input = this.#root.querySelector('input'); this.#output = this.#root.querySelector('.output'); this.#progress = this.#root.querySelector('progress'); const form = this.#root.querySelector('form'); form.addEventListener('submit', this.onSubmit.bind(this)); } static get observedAttributes() { return [ 'placeholder', // placeholder for the input field 'hello', // initial fake message by the AI 'url', // URL to the AI server endpoint ]; } /** * Called when the DOM has been connected * * We initialize the attribute based states here */ connectedCallback() { this.#input.placeholder = this.getAttribute('placeholder') || 'Your question...'; this.displayMessage(this.getAttribute('hello') || 'Hello, how can I help you?', {}); this.restoreHistory(); this.stopProgress(); // initializes the visibility states } /** * Define the web component's internal styles * * @returns {HTMLStyleElement} */ getStyle() { const style = document.createElement('style'); style.textContent = ` :host { --color-human: #ebd8b2; --color-ai: #c6dbf2; --color-link: #4881bf; } * { box-sizing: border-box; font-family: sans-serif; } form { clear: both; } progress, input { width: 100%; } input { padding: 0.25em; font-size: 1.2em; } progress { color: var(--color-link); accent-color: var(--color-link); } a { color: var(--color-link); } .output p { border-radius: 0.25em; clear: both; padding: 0.5em 1em; position: relative; } .output p::before { content: ""; width: 0px; height: 0px; position: absolute; top: 0; } .output p.user { background-color: var(--color-human); float: right; margin-right: 2em; border-top-right-radius: 0; } .output p.user::before { right: -1em; border-left: 0.5em solid var(--color-human); border-right: 0.5em solid transparent; border-top: 0.5em solid var(--color-human); border-bottom: 0.5em solid transparent; } .output p.ai { background-color: var(--color-ai); float: left; margin-left: 2em; border-top-left-radius: 0; } .output p.ai::before { left: -1em; border-left: 0.5em solid transparent; border-right: 0.5em solid var(--color-ai); border-top: 0.5em solid var(--color-ai); border-bottom: 0.5em solid transparent; } `; return style; } /** * Save history to session storage */ saveHistory() { sessionStorage.setItem('ai-chat-history', JSON.stringify(this.#history)); } /** * Load the history from session storage and display it */ restoreHistory() { const history = sessionStorage.getItem('ai-chat-history'); if (history) { this.#history = JSON.parse(history); this.#history.forEach(row => { this.displayMessage(row[0]); this.displayMessage(row[1], row[2]); }); } } /** * Submit the given question to the server * * @param event * @returns {Promise