Frontmatter of the page which consists of an AI generated image of a chatbot.

Tutorial: Chatbot in AstroJS with CloudFlare Workers AI (Part 3) - Creating the Chatbot Component in AstroJS

Chat with an AI

Finally, let’s build the chatbot client component using AstroJS to talk to our endpoint. This page also shows off the final result. You can find a chat bubble icon like this at the bottom of the page to start chatting:

Bubble chat icon to open it

Setting up the Component

First, create a new file named ChatBot.astro in the src/components directory. The HTML code will be very simple (you can add complexity depending on your needs):

src/components/ChatBot.astro
1
<aside class="chat closed">
2
<header class="chat-header">
3
<h2>Chat with an AI</h2>
4
</header>
5
<ul id="messages">
6
<!-- Here are the history of messgaes -->
7
</ul>
8
<form id="chatForm">
9
<input id="message" type="text" autocomplete="off" />
10
<button id="submitButton" type="submit">Send</button>
11
</form>
12
</aside>

As you can see, the HTML is pretty basic, using just three elements:

  1. A header to add a title to the chat (optional).
  2. An unordered list to show the message history. We’ll fill this with JavaScript later.
  3. A form with an input field and a submit button. Type your message in the input field and hit submit to send it to the AI.

Note that adding autocomplete="off" to the input field can prevent the browser from suggesting messages.

The real magic happens in the JavaScript code. To keep things organized, I have created a separate file called chatbot_utils.ts in the src/lib directory for the logic. This file will export two functions: initChat() to set up the chatbot and sendMessage() to send messages to the server.

Let’s import these functions into our Astro component and use them like this:

src/components/ChatBot.astro
1
import { initChat, sendMessage } from "src/lib/chatbot_utils";
2
3
document.addEventListener("DOMContentLoaded", () => {
4
initChat();
5
document.getElementById("chatForm")?.addEventListener("submit", async function (e) {
6
e.preventDefault();
7
await sendMessage();
8
});
9
});

Understanding chatbot_utils.ts

Let’s dive into the chatbot_utils.ts file in the src/lib directory where the core chatbot logic resides.

The initChat() Function

First up, we’ll explore the initChat() function, which sets everything up when the page loads.

src/lib/chatbot_utils.ts
1
export function initChat() {
2
const $messages = document.getElementById("messages") as HTMLUListElement;
3
const messages = retrieveMessages();
4
$messages.innerHTML = "";
5
if (messages.length === 0) {
6
messages.push({
7
role: "assistant",
8
content:
9
"Hi, welcome to my chat! I am a friendly assistant. Go ahead and send me a message. 😄",
10
});
11
storeMessages(messages);
12
}
13
for (const msg of messages) {
14
$messages.appendChild(createChatMessageElement(msg).chatElement);
15
$messages.scrollTop = $messages.scrollHeight;
16
}
17
}

Improved Text:

This function starts by loading the previous conversation from session storage using the retrieveMessages() function (we’ll cover that here). It then clears the message history display and either adds a welcome message if there’s no saved conversation or repopulates the chat with the loaded messages.

The sendMessage() function

The sendMessage() function is the heart of the chatbot’s communication. It’s responsible for taking the user’s input, formatting it into a suitable request, and sending it to the backend endpoint. Upon receiving a response, the function parses the data, creates appropriate HTML elements representing the AI’s response and the user’s message, and appends them to the message history.

src/lib/chatbot_utils.ts
1
export async function sendMessage() {
2
const $input = document.getElementById("message") as HTMLInputElement;
3
const $messages = document.getElementById("messages") as HTMLUListElement;
4
const messages = retrieveMessages();
5
6
// Create user message element
7
const userMsg: RoleScopedChatInput = { role: "user", content: $input.value };
8
messages.push(userMsg);
9
$messages.appendChild(createChatMessageElement(userMsg).chatElement);
10
11
const payload = messages;
12
$input.value = "";
13
...
14
}

First, we handle the user input. We grab the input field, chat history element, and load the message history from session storage. We create a RoleScopedChatInput object using the input value and add it to the message history. Then, we create an HTML list item for the user’s message using createChatMessageElement() (we’ll see that later) and append it to the chat history. Finally, we clear the input field.

Next, let’s see how to generate and display the assistant’s response:

src/lib/chatbot_utils.ts
1
import { marked } from "marked";
2
3
export async function sendMessage() {
4
...
5
var assistantMsg: RoleScopedChatInput = { role: "assistant", content: "" };
6
const { chatElement, text } = createChatMessageElement(assistantMsg);
7
$messages.appendChild(chatElement);
8
const assistantResponse = text;
9
// Scroll to the latest message
10
$messages.scrollTop = $messages.scrollHeight;
11
12
const response = await fetch("/api/chatbot", {
13
method: "POST",
14
headers: {
15
"Content-Type": "text/event-stream",
16
},
17
body: JSON.stringify(payload),
18
});
19
20
if (response.body) {
21
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
22
23
while (true) {
24
const { value, done } = await reader.read();
25
if (done) {
26
break;
27
}
28
29
assistantMsg.content += value;
30
// Continually render the markdown => HTML
31
assistantResponse.innerHTML = marked.parse(assistantMsg.content) as string;
32
$messages.scrollTop = $messages.scrollHeight;
33
}
34
}
35
messages.push(assistantMsg);
36
storeMessages(messages);
37
}

We create the assistant message following the type RoleScopedChatInput as before and we append it to the chat history. Note that the content field is empty, we are going to fill it later with the response from the AI model. The createChatMessageElement() also return the text element in this case to be able to append the response to the message. We use the $messages.scrollTop = $messages.scrollHeight; line to scroll to the latest message everytime the chat is updated.

Following this, the function sends a fetch request to the /api/chatbot endpoint, including the current message history. As the endpoint yields a stream of response data, the function processes each chunk by appending it to the assistant’s message and converting any Markdown formatting using the marked library. Once the full response is received, the completed assistant message element is added to the chat history, and the updated message history is stored in session storage.

Utility functions

We’ve used three helper functions so far: retrieveMessages(), storeMessages(), and createChatMessageElement(). Let’s break down the first two, which are pretty straightforward. They handle storing and retrieving the conversation history in session storage.

To store the messages, we convert the list of RoleScopedChatInput objects into a string using JSON.stringify(). For retrieval, we parse the stored string back into a list of objects using JSON.parse():

src/lib/chatbot_utils.ts
1
function retrieveMessages() {
2
const msgJSON = sessionStorage.getItem("messages");
3
if (!msgJSON) {
4
return [];
5
}
6
return JSON.parse(msgJSON);
7
}
8
9
function storeMessages(msgs: RoleScopedChatInput[]) {
10
sessionStorage.setItem("messages", JSON.stringify(msgs));
11
}

The last piece of the puzzle is the createChatMessageElement() function. This function takes a RoleScopedChatInput object as input and creates an HTML list item (li) element to represent the message. Inside the list item, it creates a text element (div) to hold the actual message content. This function is crucial for building the visual representation of the chat conversation.

1
const date = new Date();
2
3
// Based on the message format of `{role: "user", content: "Hi"}`
4
function createChatMessageElement(msg: RoleScopedChatInput) {
5
// The structure is li>(header>h3+span)+p
6
const $li = document.createElement("li");
7
const $header = document.createElement("header");
8
const $text = document.createElement("div");
9
$text.classList.add("text");
10
const timestamp = `${date.getHours()}:${("00" + date.getMinutes()).slice(-2)}`;
11
12
if (msg.role === "assistant") {
13
$li.classList.add("bot");
14
$header.innerHTML = `<h3>Assistant</h3><span>${timestamp}</span>`;
15
if (msg.content === "") {
16
const $loader = document.createElement("span");
17
$loader.classList.add("loader");
18
$text.appendChild($loader);
19
} else {
20
$text.innerHTML = marked.parse(msg.content) as string;
21
}
22
} else if (msg.role === "user") {
23
$li.classList.add("user");
24
$header.innerHTML = `<h3>User</h3><span>${timestamp}</span>`;
25
$text.innerHTML = `<p>${msg.content}</p>`;
26
} else {
27
$header.innerHTML = `<h3>System</h3><span>${timestamp}</span>`;
28
$text.innerHTML = "<p>Error, no role identified</p>";
29
}
30
$li.appendChild($header);
31
$li.appendChild($text);
32
return { chatElement: $li, text: $text };
33
}

Improved Text:

The createChatMessageElement() function determines whether the message is from the user or the assistant and applies appropriate classes and header text accordingly. For assistant messages, an optional loading indicator (span with the class loader) can be added to display while the response is being processed. You can style this loader to show a loading animation.

To display timestamps, a Date object is created, and its timestamp is extracted. his timestamp is then included in the message header. You can see an example of this in the chat on this page.

Using the chatbot component in Astro

Finally, to use your chatbot in your AstroJS project, import it into your Astro page and initialize it:

// src/pages/index.astro import ChatBot from "@/components/ChatBot.astro";
<Layout>
<ChatBot />
<Layout /></Layout
>

Conclusion

This tutorial has guided you through building a basic chatbot component using AstroJS. The complete code can be found in the GitHub repository.

While you acknowledge there’s room for improvement in the code, it serves as a solid foundation for understanding chatbot development in AstroJS using CloudFlare AI models. Feel free to leave comments with feedback or suggestions for further improvement. Additionally, the discussion section is open for any questions you may have about specific parts of the tutorial.

I hope you found this tutorial helpful!