DEV Community

Cover image for Building a Smarter Chatbot with OpenAI Assistant API and Streaming(React & Node.js)
Nenlap Jahota
Nenlap Jahota

Posted on

2 2 1 1 1

Building a Smarter Chatbot with OpenAI Assistant API and Streaming(React & Node.js)

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

  1. Install Node.js and npm (if they are not already installed)
  2. 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
Enter fullscreen mode Exit fullscreen mode

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,
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
  });
Enter fullscreen mode Exit fullscreen mode

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");   
  });

});
Enter fullscreen mode Exit fullscreen mode

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
    });



Enter fullscreen mode Exit fullscreen mode

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

  1. Create a new React app by running the command (where “chat-app” is the name of your app)

    npx create-react-app chat-app

  2. Install dependencies

    cd chat-app
    npm install axios

  3. 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;
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode
  • Closing Previous EventSource Connection:
if (eventSourceRef.current) {
  eventSourceRef.current.close();
}
Enter fullscreen mode Exit fullscreen mode

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;
    });
  }
};
Enter fullscreen mode Exit fullscreen mode
  • 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();
};
Enter fullscreen mode Exit fullscreen mode

We also handle onopen and onerror events to log connection status and handle errors.

  • Storing the EventSource Instance:
eventSourceRef.current = events;
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

Finally, this is what the output of our code should look like:

chatbot interface

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)

Billboard image

Create up to 10 Postgres Databases on Neon's free plan.

If you're starting a new project, Neon has got your databases covered. No credit cards. No trials. No getting in your way.

Try Neon for Free →