21 KiB
How I built RayAI into this Very Blog.
In my previous post, Building a Feature-Rich Blog Platform with Node.js, Express, and Markdown, I walked through the technical architecture of a blog platform designed for flexibility, performance, and ease of content management. Today, I’m introducing a new feature: RayAI — an interactive AI chat system integrated directly into the blog.
RayAI allows visitors to ask questions, get real-time responses, and even interact with the blog content on a deeper level. Think of it as an intelligent assistant embedded within the blog, ready to help users understand, explore, and engage with the content. This is not just a simple Q&A bot; RayAI can handle markdown, provide code snippets, and even let users copy content for their own use.
In this post, I’ll take you through every step of integrating RayAI into the blog, from the front-end interface to the back-end API handling. Let's dive into the details of this integration and see how it all comes together.
Source
https://git.ssh.surf/snxraven/ravenscott-blog
Why Implement RayAI?
The primary motivation behind RayAI is to enhance user engagement. Static blog posts are great, but they often leave the user with questions or areas where they might want further explanation. RayAI bridges that gap by offering:
- Real-time Chat: Users can interact directly with the AI to get instant responses to questions.
- Markdown Support: RayAI interprets markdown, making it perfect for developers who might want to see code snippets or explanations in a well-formatted way.
- Code Snippet Copying: Users can easily copy code from AI responses, reducing the time they would spend manually copying and pasting.
- Interactive Tools: RayAI includes utilities for checking logs, system stats, and even fetching specific data directly within the chat interface.
With these features in mind, let's explore how RayAI was integrated into the blog platform.
The Front-End: Creating the Chat Interface
The front-end implementation of RayAI begins with the chat.ejs
file, which defines the structure of the chat interface. This file includes the layout, user input area, and the section for displaying chat messages. It also leverages Bootstrap for styling and FontAwesome for icons, keeping the UI clean and responsive.
Setting Up the HTML Structure
Here's the code for chat.ejs
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="<%= process.env.OWNER_NAME %>'s Blog">
<title><%= title %> | <%= process.env.OWNER_NAME %>'s Blog</title>
<!-- Bootstrap and Custom Styles -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
<link rel="stylesheet" href="<%= process.env.HOST_URL %>/css/chat.css">
</head>
<body class="bg-dark text-white">
<!-- Chat Container -->
<div class="chat-container">
<!-- Navbar -->
<nav class="navbar navbar-expand-lg navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="<%= process.env.HOST_URL %>"><%= process.env.SITE_NAME %></a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
<% menuItems.forEach(item => { %>
<li class="nav-item">
<a class="nav-link" href="<%= item.url %>" <%= item.openNewPage ? 'target="_blank"' : '' %>><%= item.title %></a>
</li>
<% }) %>
</ul>
</div>
</div>
</nav>
<!-- Chat Box -->
<div class="chat-box">
<div id="messages" class="messages"></div>
<!-- Input area -->
<div class="input-area">
<textarea id="messageInput" class="form-control mb-2" rows="3" placeholder="Type your message..." onkeydown="handleKeyDown(event)" autofocus></textarea>
<div class="d-flex justify-content-between">
<button class="btn btn-secondary" onclick="resetChat()">Reset Chat</button>
<div id="loading" class="spinner-border text-primary" role="status" style="display: none;">
<span class="visually-hidden">Loading...</span>
</div>
<button class="btn btn-primary" onclick="sendMessage()">Send Message</button>
</div>
</div>
</div>
</div>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
<!-- Custom Chat JS -->
<script src="<%= process.env.HOST_URL %>/js/chat.js"></script>
</body>
</html>
Key Sections in the HTML
- Navbar: Displays the blog title and navigation items dynamically loaded from the
menuItems
array. - Chat Box: This is where the chat messages between the user and RayAI are displayed. Each message is dynamically added via JavaScript.
- Input Area: Includes a textarea for input and two buttons—one for sending the message, and the other for resetting the chat.
Styling with Bootstrap and FontAwesome
The Bootstrap CSS framework is used to ensure the page is responsive and user-friendly. I also included FontAwesome for icons, like the send button and the loading spinner, ensuring the interface looks polished.
Handling User Input and Chat Logic: chat.js
Now that the front-end structure is in place, let’s look at the JavaScript that powers the chat functionality. This file, chat.js
, is responsible for:
- Handling key events (like sending messages when the user presses Enter)
- Interacting with the AI via API calls
- Displaying messages in the chat window
- Managing the loading indicator while waiting for responses
Handling Key Events and Sending Messages
The key event handler ensures that when the user presses "Enter", the message is sent without needing to click the "Send" button.
// Handles key down event to send message on Enter
function handleKeyDown(event) {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
sendMessage();
}
}
This function ensures that users can submit messages quickly by hitting Enter, making the chat experience smooth.
Sending Messages to the API
The sendMessage
function is the core of RayAI's interaction. It takes the user input, sends it to the AI backend, and displays the AI's response.
// Sends a message to the chat API
async function sendMessage() {
const messageInput = document.getElementById('messageInput');
let message = messageInput.value.trim();
if (message === '') return;
// Encode the message to avoid XSS attacks
message = he.encode(message);
// Display the user's message in the chat
displayMessage(message, 'user');
messageInput.value = ''; // Clear the input
toggleLoading(true); // Show loading indicator
try {
const response = await fetch('https://infer.x64.world/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ message: message })
});
if (response.ok) {
const data = await response.json();
displayMessage(data.content, 'assistant');
} else {
handleErrorResponse(response.status);
}
} catch (error) {
displayMessage('Error: ' + error.message, 'assistant');
} finally {
toggleLoading(false); // Hide loading indicator
}
}
- API Interaction: This function sends the user’s message to the API endpoint
https://infer.x64.world/chat
, which handles the AI interaction. - XSS Protection: The message is encoded using
he.encode()
to prevent cross-site scripting attacks. - Message Display: After the message is sent, it’s displayed in the chat window and the loading spinner is shown while waiting for the response.
Displaying Chat Messages
Once the API responds, the message is displayed in the chat box using the displayMessage
function. This function takes care of rendering both user and AI messages in the chat window.
// Displays a message in the chat window
function displayMessage(content, sender) {
const messages = document.getElementById('messages');
const messageElement = document.createElement('div');
messageElement.classList.add('message', sender);
// Decode HTML entities and
render Markdown
const decodedContent = he.decode(content);
const htmlContent = marked(decodedContent);
messageElement.innerHTML = htmlContent;
messages.appendChild(messageElement);
messages.scrollTop = messages.scrollHeight; // Scroll to the bottom of the chat
// Highlight code blocks if any
document.querySelectorAll('pre code').forEach((block) => {
hljs.highlightElement(block);
if (sender === 'assistant') {
addCopyButton(block); // Add copy button to code blocks
}
});
// Add "Copy Full Response" button after each assistant response
if (sender === 'assistant') {
addCopyFullResponseButton(messages, messageElement);
}
}
Key features of this function:
- Markdown Support: RayAI’s responses are parsed as Markdown using
marked
, allowing code snippets and formatted text to display beautifully. - Scroll Management: Automatically scrolls the chat window to the bottom when a new message is added.
- Code Highlighting: If the AI responds with a code snippet, it’s highlighted using highlight.js, improving readability for developers.
- Copy Functionality: Adds a "Copy" button to code blocks, allowing users to copy snippets with a single click.
Adding Utility Buttons
RayAI goes beyond just chat by offering interactive tools directly within the chat interface. Here's how we implement the "Copy Full Response" and "Copy Code" buttons.
// Adds "Copy Full Response" button below the assistant response
function addCopyFullResponseButton(messagesContainer, messageElement) {
const copyFullResponseButton = document.createElement('button');
copyFullResponseButton.classList.add('copy-button');
copyFullResponseButton.textContent = 'Copy Full Response';
copyFullResponseButton.addEventListener('click', () => copyFullResponse(messageElement));
messagesContainer.appendChild(copyFullResponseButton);
}
This button copies the entire AI response, including any markdown or code blocks, into the user’s clipboard in Markdown format.
Certainly! Here's an extended and more detailed version of the section on The Backend: RayAI API Integration.
The Backend: RayAI API Integration
The backbone of RayAI’s functionality lies in how it communicates with an external AI service, processes the user's inputs, and returns intelligent, contextual responses. In this section, I will break down how RayAI handles requests, the architecture behind it, and the measures taken to ensure smooth interaction between the blog platform and the AI API.
API Overview and How It Works
At its core, the AI powering RayAI is hosted at an external endpoint, https://infer.x64.world/chat
. This API accepts POST requests with user input and returns a generated response. To integrate this with the blog platform, the JavaScript on the front-end captures the user's message, sends it to the API, and processes the AI's response for display within the chat interface.
The key objectives of this integration are:
- Seamlessly send user input to the AI API.
- Receive and display the response from the API in real time.
- Handle network issues or server overloads gracefully.
- Secure communication to prevent potential vulnerabilities, such as cross-site scripting (XSS) or API abuse.
Sending Requests to the AI API
The main logic for sending user messages to the API is contained in the sendMessage
function. Let’s go deeper into the code to explain how the back-and-forth communication is handled.
Here’s the function once again:
async function sendMessage() {
const messageInput = document.getElementById('messageInput');
let message = messageInput.value.trim();
if (message === '') return;
// Encode the message to avoid XSS attacks
message = he.encode(message);
// Display the user's message in the chat
displayMessage(message, 'user');
messageInput.value = ''; // Clear the input
toggleLoading(true); // Show loading indicator
try {
// Send the user's message to the AI API
const response = await fetch('https://infer.x64.world/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ message: message })
});
// Handle the API response
if (response.ok) {
const data = await response.json();
displayMessage(data.content, 'assistant');
} else {
handleErrorResponse(response.status);
}
} catch (error) {
displayMessage('Error: ' + error.message, 'assistant');
} finally {
toggleLoading(false); // Hide loading indicator
}
}
Breaking Down the Code
-
Input Validation:
Before sending any request, the user’s input is first trimmed to remove unnecessary whitespace. If the input is empty (i.e., no text was entered), the function exits early to avoid sending unnecessary requests. -
XSS Protection:
User input is encoded usinghe.encode()
. This is crucial for preventing cross-site scripting (XSS) attacks, where malicious users could inject harmful code into the chat. By encoding the input, we ensure that any special characters are safely transformed, so the AI API only processes clean text. -
Displaying the User’s Message:
Once the message is encoded, it is immediately displayed in the chat interface (before even getting a response from the AI). This gives the user a smooth, instant feedback loop and ensures that their input is visually confirmed. -
Sending the Request:
Thefetch()
function sends the message as a POST request to the external API (https://infer.x64.world/chat
). Here’s what happens in the background:- The request contains a JSON object with the key
message
holding the user’s input. - Headers are set to indicate that the request body is JSON (
'Content-Type': 'application/json'
), which is what the API expects.
- The request contains a JSON object with the key
-
Handling the Response:
- Success: If the response from the API is successful (status code 200), the response body (a JSON object) is parsed. The content generated by the AI is then passed to the
displayMessage
function to be shown as an AI response in the chat interface. - Error: If the response is not successful (e.g., status codes like 400 or 500), the
handleErrorResponse
function is called to manage error feedback to the user.
- Success: If the response from the API is successful (status code 200), the response body (a JSON object) is parsed. The content generated by the AI is then passed to the
-
Loading Indicator:
While waiting for the API to respond, a loading indicator (a spinner) is displayed, improving user experience by signaling that the system is processing their request. This indicator is hidden once the response is received.
Error Handling and Rate Limiting
Integrating a third-party API like this comes with challenges, such as network errors, rate limits, and potential server overloads. These are handled through robust error-checking and failover mechanisms.
function handleErrorResponse(status) {
if (status === 429) {
displayMessage('Sorry, I am currently too busy at the moment!', 'assistant');
} else {
displayMessage('Error: ' + status, 'assistant');
}
}
Handling Rate Limiting (429 Status Code)
When multiple users interact with RayAI simultaneously, or when the API receives too many requests in a short span of time, it may respond with a 429 (Too Many Requests) status. This is known as rate limiting, a mechanism used by APIs to prevent overloading.
In such cases, RayAI responds with a friendly message like:
"Sorry, I am currently too busy at the moment!"
This ensures users know the service is temporarily unavailable, rather than simply failing silently. Additionally, retry logic could be implemented if needed, to automatically attempt a new request after a short delay.
Handling Network and Other Errors
Other common errors include server issues (status code 500) or connection issues (e.g., when the user’s internet connection is unstable). These are captured within the catch
block of the try-catch
structure. In case of failure, an error message such as the following is displayed:
"Error: {error.message}"
This ensures that even if something goes wrong, the user receives a clear explanation of the issue.
Securing the API
While RayAI’s front-end handles much of the logic, securing the API interaction is essential to prevent abuse, data breaches, or malicious attacks.
XSS and Input Validation
As mentioned earlier, we encode all user input using he.encode()
before sending it to the API. This ensures that special characters (such as <
, >
, and &
) are converted into their HTML-safe equivalents, protecting the application from cross-site scripting attacks.
Rate Limiting and Abuse Prevention
The external AI service includes built-in rate limiting (as indicated by the 429 error code), but further measures could be implemented to prevent API abuse:
- IP-Based Rate Limiting: You can restrict the number of API requests allowed from a single IP address within a given time window. This prevents users or bots from spamming the service with excessive requests.
- Authentication: Adding a layer of authentication (such as API keys or OAuth) could ensure that only authorized users have access to RayAI’s features.
Enhancing AI Responses
RayAI is more than just a simple chatbot. The backend API offers several features that enhance the experience:
-
Markdown Rendering:
RayAI’s responses often include formatted text, especially when answering technical questions. The responses are processed using marked.js to ensure markdown syntax is rendered correctly in the chat. -
Code Highlighting:
If the AI responds with a code snippet, it’s automatically highlighted using highlight.js, which provides syntax highlighting for over 180 programming languages. This is particularly useful for developers who interact with RayAI. -
Copy Buttons:
Each response that includes code also has a "Copy" button. This feature is implemented using custom JavaScript, which allows users to copy entire code blocks or full responses to their clipboard in a single click.
Additional Tools in RayAI
Apart from chat functionality, RayAI also provides access to interactive tools through the chat interface. These include:
- Live Logs: Users can view live system logs in real-time by clicking a "Live Log" button.
- System Stats: Displays system metrics like CPU, memory, and GPU stats through the "GPU Stats" button.
- Reset Chat: This feature allows users to clear the current chat history and reset the conversation.
It sends a POST request to
https://infer.x64.world/reset-conversation
, clearing the session data.
These tools are essential for advanced users who want to monitor system performance or interact with RayAI in more technical ways.
Final Thoughts: The Power of RayAI
By integrating RayAI, the blog has evolved from a static platform into a dynamic, interactive experience. RayAI provides real-time interactions, makes technical content more accessible, and helps users engage with blog posts on a deeper level. Features like code highlighting, markdown rendering, and interactive tools bring immense value to both developers and casual readers.
In the future, RayAI could be extended even further with additional features like natural language search for blog posts, personalized recommendations, or even interactive tutorials. The possibilities are endless.
For now, I’m thrilled with how RayAI enhances the blog, and I can’t wait to see how users engage with it.
If you want to try it yourself, head over to the RayAI Chat Page and start a conversation!