Imagine using a chatbot that does not just produce generic responses. With the release of openai's assistant api with tools like File Search, chatbots become intelligent assistants, seamlessly accessing and processing information from your documents or internal knowledge bases. No longer limited to generic responses, chatbots can now tailor their answers based on your specific needs and the wealth of information at their fingertips.
This tutorial uses React to create a simple page to interact with the chatbot and NodeJs to call the api endpoints.
Imagine a chatbot that can not only hold conversations but also access your documents and knowledge base to provide more relevant and helpful responses!
This tutorial guides you through creating a basic chatbot powered by the OpenAI Assistant API with File Search, using React for the frontend and Node.js for the backend.
What We'll Build:
A simple React chat interface where you can interact with the chatbot.
A Node.js backend server handling communication with the OpenAI Assistant API.
Integration of Streaming functionality (Optional, for advanced users).
Prerequisites:
- Basic knowledge of React and Node.js
- An OpenAI API key (obtainable from your OpenAI account)
- An existing OpenAI assistant with necessary tools and files configured. You can learn more about that here
- Your assistant's ID from your account dashboard
Let’s get to it!
Step 1: Setting Up your Development Environment
- Install Node.js and npm (if they are not already installed)
- Open a terminal or command prompt in your preferred directory.
Step 2: Setting Up Your Node.js server
We need to install express.js, cors, body-parser, dotenv and openai. In your terminal, run this code:
npm install express cors body-parser dotenv openai
Declare your API key & Assistant ID as environment variables: in your .env
OPENAI_API_KEY=your-api-key
ASSISTANT_ID=your-assistant-Id
Then, we can get started with the code:
// import the required dependencies
// instantiate open ai
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
Declearing some variables:
let streamedResponse = ''; //string response from api as it is being generated by assistant
let clients = []; //array to hold client info making request to /api/streamRequest
Since we plan to stream responses to the React application as it is being generated, we will declear a function to handle that:
function sendEventsToAll(streamedResponse, textEvent) {
const responseObject = {
role: "assistant",
content: streamedResponse,
textEvent: textEvent
};
// Loop through connected clients and send data using server-sent events
clients.forEach((client) => {
client.res.write(`data: ${JSON.stringify(responseObject)}\n\n`);
//note: the new line at the end is mandatory. It Signals the end of a response
});
The above function takes two arguments, streamedResponse which is a string of text that is being generated by the assistant and textEvent which could be either textCreated, textDelta, or end. You can read more about assistant events here
We then decleare responseObject and send this object to our React application and we will be making use of server-sent-events (SSE) to do this. SSE allows the server to push updates (in this case, the streamed response) to the connected clients in real-time without requiring the client to constantly poll for new data.
Next, we'll create an endpoint that uses Server-Sent Events (SSE) to push data to connected clients.
app.get("/api/streamUserRequest", (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Connection': 'keep-alive',
'Cache-Control': 'no-cache'
});
const newClient = {
res,
};
clients.push(newClient);
req.on("close", () => {
console.log("Client disconnected");
});
});
We'll also create a post endpoint for our API to recieve query from our React application and generate a response.
app.post("/api/openai/chatbot", async (req, res) => {
const { query } = req.body;
const question = query;
//create a new thread
const newThread = await openai.beta.threads.create();
//create a message
const message = await openai.beta.threads.messages.create(newThread.id, {
role: "user",
content: query,
});
//begin to stream response
const stream = openai.beta.threads.runs
.createAndStream(newThread.id, {
assistant_id: assistant_id, // Ensure this variable is correctly defined
})
.on("textCreated", (text) => {
sendEventsToAll(streamedResponse, "textCreated")
})
.on("textDelta", (textDelta) => {
//append the newly generated text to the streamedResponse variable
streamedResponse+=textDelta.value
sendEventsToAll(streamedResponse, "textDelta")
})
.on("end", () => {
clients = [] //clean up client array
streamedResponse = '' //clean up stream a array for new request
});
We start by destructuring the request for the user's query. Create a thread and start streaming the response. The assistant's event handlers call the sendEventsToAll function with two arguments; streamedResponse which is the string of text generated and textEvent which is the assistant event in this case textCreated, textDelta or end.
Step 3: Setting Up the React application
-
Create a new React app by running the command (where “chat-app” is the name of your app)
npx create-react-app chat-app
-
Install dependencies
cd chat-app
npm install axios
-
Start the development server by running the command
npm start
That’s it! Your new React app should now be running at http://localhost:3000. You can open the app in your code editor and start making changes to the code to customize it as needed.
Step 4: Create the Chatbot Component
import { useState, useRef, useEffect } from "react";
import axios from "axios";
import ReactMarkdown from "react-markdown";
// ChatWindow component definition
const ChatWIndow = () => {
// State variables
const [message, setMessage] = useState(""); // Stores the current user input message
const [history, setHistory] = useState([]); // Stores the chat history (user and assistant messages)
const lastMessageRef = useRef(null); // Ref to scroll to the last message
const eventSourceRef = useRef(null); // Ref to manage EventSource connection
const [loading, setLoading] = useState(false); // Loading state for the request
// Handle message submission
const handleClick = async () => {
if (!message) return; // Prevent sending empty messages
const events = new EventSource("http://localhost:4241/api/streamUserRequest");
// Close the old EventSource connection if it exists
if (eventSourceRef.current) {
eventSourceRef.current.close();
}
// Update chat history with the new user message
setHistory((oldHistory) => [
...oldHistory,
{ role: "user", content: message },
]);
setMessage(""); // Clear the input field
setLoading(true); // Set loading state to true
const thread_Id = localStorage.getItem("current_thread_id"); // Retrieve current thread ID from local storage
try {
// Send user message to the chatbot API
const response = await axios.post(
"/api/openai/chatbot",
{
query: message,
history: history,
thread_id: thread_Id,
}
);
// Check for a valid response and update thread ID in local storage
if (!response.data) throw new Error("Network response was not ok");
localStorage.setItem("current_thread_id", response.data.threadId);
// Handle incoming messages from the server
events.onmessage = (event) => {
const parsedData = JSON.parse(event.data);
if (parsedData.textEvent === "textCreated") {
// Add new assistant message to history
setHistory((oldHistory) => [...oldHistory, parsedData]);
}
if (parsedData.textEvent === "textDelta") {
// Update the last assistant message with new content
setHistory((oldHistory) => {
const updatedHistory = [...oldHistory];
updatedHistory[updatedHistory.length - 1] = {
...updatedHistory[updatedHistory.length - 1],
content: parsedData.content,
};
return updatedHistory;
});
}
};
// Log successful connection to the server
events.onopen = () => console.log("SSE connection opened successfully.");
// Handle errors in the EventSource connection
events.onerror = (error) => {
console.error("SSE connection encountered an error:", error);
setLoading(false);
events.close(); // Clean up event source on error
};
// Store the new EventSource object in the useRef
eventSourceRef.current = events;
} catch (error) {
console.error("Error sending message:", error);
// Handle error here (e.g., show error message to user)
} finally {
setLoading(false); // Reset loading state
}
};
// Scroll to the bottom of the chat history after each message
useEffect(() => {
if (lastMessageRef.current) {
lastMessageRef.current.scrollIntoView({ behavior: "smooth" });
}
}, [history]);
// Render the chat interface
return (
<div className='mainWindow'>
<section className='mt-10 flex justify-center fixed bg-blue-500 shadow-xl shadow-black text-2xl'>
<h1 className='items-center text-red-600'>Chatbot Assistant</h1>
</section>
<div className='chatWindow'>
{history.map((message, idx) => {
const isLastMessage = idx === history.length - 1; // Check if this is the last message
switch (message.role) {
case "assistant":
return (
<div
ref={isLastMessage ? lastMessageRef : null}
key={idx}
className="flex gap-2"
>
<div className="w-auto max-w-xl break-words bg-white rounded-b-xl rounded-tr-xl text-black p-6 shadow-[0_10px_40px_0px_rgba(0,0,0,0.15)] messagebox">
<p className="titlesAssistant">AI assistant</p>
<ReactMarkdown>{message.content}</ReactMarkdown>
</div>
</div>
);
case "user":
return (
<div
className="messagebox"
key={idx}
ref={isLastMessage ? lastMessageRef : null}
>
<p className="titlesYou">You</p>
{message.content}
</div>
);
}
})}
</div>
<div className="">
<form
className="queryForm"
onSubmit={(e) => {
e.preventDefault();
handleClick(); // Handle message submission on form submit
}}
>
{/* Input area for user messages */}
<div className="inputArea">
<textarea
aria-label="chat input"
value={message}
onChange={(e) => setMessage(e.target.value)} // Update message state on input change
placeholder="Type a message"
className="textarea"
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault(); // Prevent new line on Enter
handleClick(); // Send message on Enter key press
}
}}
/>
<button
onClick={(e) => {
e.preventDefault();
handleClick(); // Handle message submission on button click
}}
className="sendButton"
type="submit"
aria-label="Send"
disabled={!message || loading} // Disable button if no message or loading
>
<i className="fas fa-paper-plane"></i>
</button>
</div>
</form>
</div>
</div>
);
}
export default ChatWIndow;
Using EventSource in React for Real-Time Updates
In our chatbot interface, we use the EventSource API to handle real-time updates from the server. This allows us to receive server-sent events (SSE) and update the chat interface dynamically as new messages arrive.
Key Points on EventSource Usage:
- Creating an EventSource Instance:
const events = new EventSource("/api/streamUserRequest");
- Closing Previous EventSource Connection:
if (eventSourceRef.current) {
eventSourceRef.current.close();
}
Before creating a new EventSource, we close any existing connection to avoid multiple open connections.
- Handling Server-Sent Events:
events.onmessage = (event) => {
const parsedData = JSON.parse(event.data);
if (parsedData.textEvent == "textCreated") {
setHistory((oldHistory) => [...oldHistory, parsedData]);
}
if (parsedData.textEvent == "textDelta") {
setHistory((oldHistory) => {
const updatedHistory = [...oldHistory];
updatedHistory[updatedHistory.length - 1] = {
...updatedHistory[updatedHistory.length - 1],
content: parsedData.content,
};
return updatedHistory;
});
}
};
- Handling Connection Open and Error Events:
events.onopen = () => console.log("SSE connection opened successfully.");
events.onerror = (error) => {
console.error("SSE connection encountered an error:", error);
setLoading(false);
events.close();
};
We also handle onopen and onerror events to log connection status and handle errors.
- Storing the EventSource Instance:
eventSourceRef.current = events;
Finally, we store the EventSource instance in a useRef hook to manage its lifecycle and ensure it can be closed properly when needed.
Step 4: Add basic styling to the page:
/* index.css */
.mainWindow{
padding: 4rem 6rem;
}
.chatWindow{
height: 480px;
width: 100%;
margin-top: 1em;
overflow-y: auto;
resize: none;
border: none;
}
.titlesAssistant{
font-weight: 500;
color: #6d28d9;
margin-bottom: 0.5rem;
}
.titlesYou{
color: #22c55e;
margin-bottom: 0.5rem;
font-weight: 500;
}
.messagebox{
width: auto;
max-width: 36rem;
overflow-wrap: break-word;
border-bottom-right-radius: 0.75rem;
border-bottom-left-radius: 0.75rem;
background-color: #fff;
color: #000;
padding: 1.5rem ;
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
.queryForm{
flex-grow: 1;
max-height: 100%;
overflow: hidden;
resize: none;
}
.inputArea{
display: flex;
padding-bottom: 1.5rem;
height: 6rem;padding-left: 1.5rem /* 24px */;
padding-right: 1.5rem /* 24px */;
width: 100%;
position: fixed; /* Fixes the position of the div */
bottom: 0; /* Positions the div at the bottom */
left: 0; /* Aligns the div to the left side of the screen */
width: 100%;
}
.textarea{
width: 80%;
resize: none;
border-radius: 9999px;
border-color: rgb(15 23 42 / 0.1);
padding-left: 1.5rem;
padding-right: 10rem ;
padding: 25px 10rem 25px 1.5rem;
font-size: 1rem /* 16px */;
line-height: 1.5rem /* 24px */;
}
.textarea:focus{
border-color: rgb(139 92 246 );
outline: 2px solid transparent;
outline-offset: 2px;
box-shadow: #0000;
}
.sendButton{
display: flex;
align-items: center;
height: 4rem;
width: 4rem;
justify-content: center;
border-radius: 9999px;
padding: 0 0.75rem;
font-size: 0.875rem;
line-height: 1.25rem;
background-color: #22c55e;
font-weight: 600;
color: #fff;
margin-top: 20px;
margin-left: 10px;
}
.sendButton:hover{
background-color: #6d28d9;
}
.sendButton:active{
background-color: #5b21b6;
}
.sendButton:disabled{
background-color: #ede9fe;
color: #a78bfa;
}
Finally, this is what the output of our code should look like:
Conclusion
Through this tutorial, you can now build an intelligent AI assistant that will give context-specific answers and help with other tasks. Find the codebase for this tutorial on GitHub. Did you learn something new from this article? Let me know in the comments! 🎉
Top comments (0)