customElements.define( "chat-app", class extends HTMLElement { connectedCallback() { if (!this.isConnected) return; const form = window.querySelectorExt( this, "find form", ) as HTMLFormElement; const messageInput = ( form.elements as unknown as { message: HTMLInputElement } ).message; const messageArea = window.querySelectorExt( this, "find .chat-messages", ) as Element; // Handle form submissions form.addEventListener("submit", (event) => { const message = messageInput.value.trim(); // Show a validation message if the input is empty if (!message) { messageInput.setCustomValidity("Please provide a message."); // Prevent the form from submitting event.preventDefault(); return; } // Create a new message element const msg = document.createElement("div"); msg.className = "message user-message"; msg.innerHTML = `

${message}

`; // Insert the message before the pending AI message messageArea.insertBefore( msg, messageArea.querySelector(".pending-ai-message"), ); setTimeout(() => { messageInput.value = ""; messageArea.scrollTop = messageArea.scrollHeight; }); }); // Focus on the input field after the message is processed form.addEventListener("htmx:afterSettle", () => { messageInput.focus(); }); // Monitor the previousElementSibling for changes. If it's already scrolled down, // scroll down to the new message. Otherwise, leave it alone. let timeout: ReturnType | undefined; const observer = new MutationObserver(() => { if (typeof timeout == "undefined") { timeout = setTimeout(() => { if ( messageArea.scrollHeight - (messageArea.scrollTop + messageArea.clientHeight) < 50 ) { messageArea.scrollTop = messageArea.scrollHeight; } timeout = undefined; }, 200); } }); observer.observe(messageArea, { childList: true, characterData: true, subtree: true, }); } }, );