This project demonstrates an AI-powered chat system built using the OpenAI SDK. The ChatEngine module handles interactions with the OpenAI API, including message generation and context management. The ChatInterface component provides a user-friendly UI for sending messages and displaying AI responses, showcasing how to integrate advanced language models into a conversational application.
You go to LearnReactUI.dev and become a member, then download your project
When you download and open the project file, there are 2 folders, server and client.
Open the Server folder, there are 2 files in it
openai.js: After entering the API key that will allow you to access the OpenAI API SDK, we call the OpenAI Chat function by calling openai.chat.completions.create in the function. This is how the connection between the server → OpenAI is established
import OpenAI from "openai";
const openai = new OpenAI({
apiKey: "openai-api-key",
});
export async function runOpenAI(data) {
console.log("Processing react frontend development task", data);
const response = await openai.chat.completions.create({
model: "gpt-3.5-turbo",
messages: [
{
role: "system",
content:
"You are an expert in React, JavaScript, and frontend development. Based on the data provided, define the requirements, analyze the problem, and provide the best solution for building or optimizing the frontend application.",
},
{
role: "user",
content: `${data}`,
},
],
temperature: 1,
max_tokens: 1196,
top_p: 1,
frequency_penalty: 0,
presence_penalty: 0,
});
return response.choices[0];
}
index.js: This takes requests from the browser and passes them to functions in openai.js. I used Hono for this part, but you can use Express or other web servers
import { Hono } from "hono";
import { cors } from "hono/cors";
import { runOpenAI, runOpenAIStream } from "./openai";
const app = new Hono();
app.use(
"*",
cors({
origin: [
"https://onurdayibasi.dev",
"http://127.0.0.1:5173",
"http://localhost:5173",
],
})
);
app.post("/api/chat/:id", async (c) => {
const id = c.req.param("id");
const { content } = await c.req.json();
const response = await runOpenAI(content);
const result = { role: "assistant", content: response?.message?.content };
return c.json(result, 200);
});
Basically what we are doing is formatting requests from the browser to send openai.js.
The project that will work in the browser. I have simplified this project as much as possible.
npm install
npm run dev
commands and the application will be running in your local area. Let’s take a closer look at the code.
AIChatPageSample.js: Contains the entire code sample executed in the browser. Let’s take a closer look at the code sample.
We import the libraries we will use. These libraries include React, Markdown, Icon, Code Styler, JS in CSS, UI Library, Client State and Network libraries.
import { ArrowCircleUp, OpenAiLogo } from "@phosphor-icons/react";
import React from "react";
import ReactMarkdown from "react-markdown";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { darcula } from "react-syntax-highlighter/dist/esm/styles/prism";
import styled from "@emotion/styled";
import { Button, Input, Typography } from "antd";
import axios from "axios";
import { create } from "zustand";
In the 2nd section we keep the config related to the application. Backend API URL address is included.
//===============================================================
//Configs
//===============================================================
const restApiUri = "backend_api_url";
This is the part where we write Custom Hooks. There is only useKeyPressOne custom hook here. This is the hook that will send a single request when the Enter button is held down. In this way, we will not send back-to-back API requests to the server.
//===============================================================
//Custom Hooks
//===============================================================
const useKeyPressOne = (callback, targetKey) => {
const [isPressed, setIsPressed] = React.useState(false);
React.useEffect(() => {
const downHandler = (event) => {
if (event.key === targetKey && !isPressed) {
setIsPressed(true);
callback();
}
};
const upHandler = (event) => {
if (event.key === targetKey) {
setIsPressed(false);
}
};
window.addEventListener("keydown", downHandler);
window.addEventListener("keyup", upHandler);
return () => {
window.removeEventListener("keydown", downHandler);
window.removeEventListener("keyup", upHandler);
};
}, [callback, targetKey, isPressed]);
};
In this part, we create a client store where we can keep the user’s chat messages and chat messages from the ai bot.
//===============================================================
//Client Store
//===============================================================
const useResponsesStore = create((set) => ({
responses: [], // Initial state
addResponse: (response) =>
set((state) => ({
responses: [...state.responses, response], // Append the new response
})),
}));
const useKeyPressOne = (callback, targetKey) => {
const [isPressed, setIsPressed] = React.useState(false);
React.useEffect(() => {
const downHandler = (event) => {
if (event.key === targetKey && !isPressed) {
setIsPressed(true);
callback();
}
};
const upHandler = (event) => {
if (event.key === targetKey) {
setIsPressed(false);
}
};
window.addEventListener("keydown", downHandler);
window.addEventListener("keyup", upHandler);
return () => {
window.removeEventListener("keydown", downHandler);
window.removeEventListener("keyup", upHandler);
};
}, [callback, targetKey, isPressed]);
};
Layout ve Bileşenlerin Style tek bir obje içerisine topladım. Bu sayede bunun üzerinden bileşenlere giydirebileceğim
/===============================================================
//Styling
//===============================================================
const Styled = {
ChatAppLayout: styled.div`
display: flex;
flex-direction: column;
justify-content: flex-end;
align-items: center;
height: ${(props) => props.height || "100%"};
width: 100%;
`,
BottomLayout: styled.div`
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
width: 100%;
height: 100px;
`,
UserInputLayout: styled.div`
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
width: 750px;
height: 100%;
`,
SendButtonLayout: styled.div`
position: relative;
display: flex;
right: 42px;
top: 22px;
`,
ChatLayout: styled.div`
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
height: calc(100% - 100px);
overflow: auto;
`,
ChatInLayout: styled.div`
display: flex;
flex-direction: column;
width: 692px;
height: 100%;
`,
UserContent: styled.div`
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
width: 100%;
`,
UserContentText: styled.div`
display: flex;
margin: 12px 0px;
padding: 8px 16px;
border-radius: 20px;
background-color: #f0f0f0;
`,
AIContent: styled.div`
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
width: 100%;
padding: 16px 16px 16px 0px;
`,
AILogoContainer: styled.div`
display: flex;
margin-right: 16px;
align-items: flex-start;
margin-right: 16px;
height: 100%;
`,
};
It enables the detection and formatting of texts that can be formatted in Code, Markdown, etc. in chat messages.
//===============================================================
//Code Formatter
//===============================================================
const TextWithCodeParser = ({ text }) => {
// Regular expression to match ```jsx ... ``` blocks
const codeBlockRegex = /```jsx([\s\S]*?)```/g;
// Parse text to extract code blocks
const parts = [];
let lastIndex = 0;
let match;
while ((match = codeBlockRegex.exec(text)) !== null) {
// Add plain text before the code block
if (match.index > lastIndex) {
parts.push({
type: "text",
content: text.slice(lastIndex, match.index),
});
}
// Add the code block
parts.push({
type: "code",
content: match[1].trim(),
});
lastIndex = codeBlockRegex.lastIndex;
}
// Add remaining plain text
if (lastIndex < text.length) {
parts.push({
type: "text",
content: text.slice(lastIndex),
});
}
const components = {
code({ className, children, ...props }) {
<code className={className} {...props}>
{children}
</code>;
},
};
// Highlight dynamically
const processTextWithBackticks = (text) => {
const regex = /`([^`]*)`/g;
const parts = text.split(regex); // Split text into parts based on backticks
return parts.map((part, index) =>
index % 2 === 1 ? ( // Odd indexes are the content inside backticks
<code
key={index}
style={{
background: "#f5f5f5",
padding: "0 4px",
borderRadius: "4px",
}}
>
{part}
</code>
) : (
// Even indexes are plain text
<ReactMarkdown components={components} key={index}>
{part}
</ReactMarkdown>
)
);
};
return (
<div>
{parts.map((part, index) =>
part.type === "text" ? (
<Typography.Paragraph key={index}>
{processTextWithBackticks(part.content)}
</Typography.Paragraph>
) : (
<SyntaxHighlighter
key={index}
language="jsx"
style={darcula}
customStyle={{
maxWidth: "632px",
overflowX: "auto",
margin: "0 auto",
}}
>
{part.content}
</SyntaxHighlighter>
)
)}
</div>
);
};
This part actually contains our whole example. I will understand the subject by dividing it into more parts below so that the subject can be understood more clearly.
//===============================================================
//AI Chat Page Sample
//===============================================================
const AIChatPageSample = (props) => {
const [value, setValue] = React.useState("");
const [loading, setLoading] = React.useState(false);
const { responses, addResponse } = useResponsesStore();
const inputRef = React.useRef(null);
React.useEffect(() => {
const lastElementId = "msg-" + (responses.length - 1);
const element = document.getElementById(lastElementId);
if (element) {
element.scrollIntoView({ behavior: "smooth", block: "start" });
}
}, [responses.length]);
const handlePost = (apiURL, message) => {
addResponse({ role: "user", content: value });
setLoading(true);
axios
.post(apiURL, message)
.then(function (response) {
console.log("data post", response?.data);
addResponse(response?.data);
setValue("");
})
.catch(function (error) {
console.log("request failed", error);
})
.finally(function () {
setLoading(false);
setTimeout(() => {
inputRef.current.focus(), 0;
});
});
};
useKeyPressOne(() => {
if (!loading) {
handlePost(`${restApiUri}/chat/1`, { content: value });
}
}, "Enter");
return (
<Styled.ChatAppLayout height={props.height}>
<Styled.ChatLayout>
<Styled.ChatInLayout>
{responses.map((response, index) => (
<>
{response.role === "user" && (
<Styled.UserContent id={"msg-" + index} key={index}>
<Styled.UserContentText>
<Typography.Text> {response.content}</Typography.Text>
</Styled.UserContentText>
</Styled.UserContent>
)}
{response.role !== "user" && (
<Styled.AIContent id={"msg-" + index} key={index}>
<Styled.AILogoContainer>
<OpenAiLogo size={32} color="#454545" weight="thin" />
</Styled.AILogoContainer>
<TextWithCodeParser text={response.content} />
</Styled.AIContent>
)}
</>
))}
</Styled.ChatInLayout>
</Styled.ChatLayout>
<Styled.BottomLayout>
<Styled.UserInputLayout>
<Input.TextArea
ref={inputRef}
size="large"
value={value}
disabled={loading}
onChange={(e) => {
setValue(e.target.value);
}}
placeholder="Message to AIBot"
autoSize={{ minRows: 3, maxRows: 5 }}
/>
<Styled.SendButtonLayout>
<Button
type="primary"
shape="circle"
icon={<ArrowCircleUp size={42} />}
disabled={loading}
loading={loading}
onClick={() =>
handlePost(`${restApiUri}/chat/1`, { content: value })
}
/>
</Styled.SendButtonLayout>
</Styled.UserInputLayout>
</Styled.BottomLayout>
</Styled.ChatAppLayout>
);
};
First of all, useState and Zustand State parts, here the value to be kept in the chat text field, the button, all chat messages and the loading state during the process of sending this message are kept.
const [value, setValue] = React.useState("");
const [loading, setLoading] = React.useState(false);
const { responses, addResponse } = useResponsesStore();
const inputRef = React.useRef(null);
The next part is the useEffect part. Here, there is a piece of code that ensures that the screen cannot automatically scroll to this message when the AI Bot message arrives.
React.useEffect(() => {
const lastElementId = "msg-" + (responses.length - 1);
const element = document.getElementById(lastElementId);
if (element) {
element.scrollIntoView({ behavior: "smooth", block: "start" });
}
}, [responses.length]);
handlePost sends the code to the server.
const handlePost = (apiURL, message) => {
addResponse({ role: "user", content: value });
setLoading(true);
axios
.post(apiURL, message)
.then(function (response) {
console.log("data post", response?.data);
addResponse(response?.data);
setValue("");
})
.catch(function (error) {
console.log("request failed", error);
})
.finally(function () {
setLoading(false);
setTimeout(() => {
inputRef.current.focus(), 0;
});
});
};
There are 2 triggers that trigger handlePost. One when a user presses the Enter button, another when a user presses the button on the Input field This piece of code controls the pressing of the Enter button.
useKeyPressOne(() => {
if (!loading) {
handlePost(`${restApiUri}/chat/1`, { content: value });
}
}, "Enter");
Another piece of code is the onClick bound call on the button.
<Button
type="primary"
shape="circle"
icon={<ArrowCircleUp size={42} />}
disabled={loading}
loading={loading}
onClick={() => handlePost(`${restApiUri}/chat/1`, { content: value })}
/>
Below is the Prompt component where the user can enter the questions they want to ask.
<Input.TextArea
ref={inputRef}
size="large"
value={value}
disabled={loading}
onChange={(e) => {
setValue(e.target.value);
}}
placeholder="Message to AIBot"
autoSize={{ minRows: 3, maxRows: 5 }}
/>
Let’s come to the last part, listing the messages from the user and the bot
<Styled.ChatInLayout>
{responses.map((response, index) => (
<>
{response.role === "user" && (
<Styled.UserContent id={"msg-" + index} key={index}>
<Styled.UserContentText>
<Typography.Text> {response.content}</Typography.Text>
</Styled.UserContentText>
</Styled.UserContent>
)}
{response.role !== "user" && (
<Styled.AIContent id={"msg-" + index} key={index}>
<Styled.AILogoContainer>
<OpenAiLogo size={32} color="#454545" weight="thin" />
</Styled.AILogoContainer>
<TextWithCodeParser text={response.content} />
</Styled.AIContent>
)}
</>
))}
</Styled.ChatInLayout>
It’s actually quite simple to make an AI example, just focus on the parts, understand how those parts work and then put them together.