AI Chat Assistant: React Based Project

Last Updated : 21 Aug, 2025

Here is an AI Chat Assistant, a React-based web app that offers smart, interactive conversations. It uses advanced AI to understand and respond to user queries, providing a simple and engaging chat experience. Built with React, the app is fast, responsive, and easy to customize.

image

File and Folder Organization

project-structure-1

Setup

  • Install Client Dependencies: Navigate to the client folder and install the dependencies:
cd client
npm install
  • Install Server Dependencies: Navigate to the server folder and install the server-side dependencies:
cd ../server
npm install
  • Start the Client (React App):
cd client
npm run dev
  • Start the Server (Backend):
cd ../server
npm run dev

Code base

  • src
styles.css
:root {
  --bg: #0b0b0c;
  --panel: #121214;
  --muted: #a1a1aa;
  --text: #e5e7eb;
  --primary: #6366f1;
  --bubble-user: #1f2937;
  --bubble-ai: #0f172a;
  --border: #26272b;
}
* { box-sizing: border-box; }
html, body, #root { height: 100%; }
body { margin: 0; background: var(--bg); color: var(--text); font: 14px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Inter, Arial; }

.app { display: flex; flex-direction: column; height: 100vh; }
.app__header { position: sticky; top: 0; z-index: 10; display: flex; gap: 12px; align-items: center; padding: 14px 18px; background: rgba(18,18,20,0.8); backdrop-filter: blur(8px); border-bottom: 1px solid var(--border); }
.logo { width: 36px; height: 36px; display: grid; place-items: center; background: var(--primary); color: white; border-radius: 12px; font-weight: 700; }
.titles h1 { margin: 0; font-size: 16px; }
.titles p { margin: 2px 0 0; font-size: 12px; color: var(--muted); }

.chat { flex: 1; display: grid; grid-template-rows: 1fr auto; }
.chat__list { padding: 16px; max-width: 880px; width: 100%; margin: 0 auto; overflow-y: auto; }

.message { display: flex; margin: 10px 0; }
.message--right { justify-content: flex-end; }
.bubble { max-width: 80%; padding: 12px 14px; border-radius: 14px; box-shadow: 0 1px 0 rgba(0,0,0,.2); border: 1px solid var(--border); white-space: pre-wrap; }
.bubble--user { background: var(--bubble-user); }
.bubble--ai { background: var(--bubble-ai); }
.bubble--error { background: #3b0a0a; border-color: #5f1111; color: #ffd7d7; }

.bubble__header { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; opacity: .85; }
.avatar { width: 28px; height: 28px; border-radius: 10px; display: grid; place-items: center; font-size: 12px; font-weight: 700; }
.avatar--ai { background: #4f46e5; color: white; }
.avatar--user { background: #e5e7eb; color: #0b0b0c; }
.avatar--error { background: #ef4444; color: white; }

.meta { font-size: 12px; color: var(--muted); }

.composer { border-top: 1px solid var(--border); background: var(--panel); }
.composer__inner { max-width: 880px; margin: 0 auto; padding: 10px 16px; }
.box { background: #0e0f12; border: 1px solid var(--border); border-radius: 16px; padding: 8px; }
textarea.input { width: 100%; background: transparent; color: var(--text); border: 0; outline: 0; resize: none; padding: 10px 12px; font: inherit; }
.toolbar { display: flex; align-items: center; justify-content: space-between; padding: 6px 8px 2px; }
button.btn { display: inline-flex; align-items: center; gap: 8px; padding: 8px 12px; border-radius: 12px; border: 1px solid var(--border); background: var(--primary); color: white; cursor: pointer; }
button.btn:disabled { opacity: .5; cursor: not-allowed; }
button.ghost { background: transparent; color: var(--text); }
.small { font-size: 12px; color: var(--muted); }
.copy { font-size: 12px; padding: 4px 8px; border-radius: 8px; border: 1px solid var(--border); background: transparent; color: var(--text); cursor: pointer; }

.spinner { width: 16px; height: 16px; border: 2px solid rgba(255,255,255,.3); border-top-color: white; border-radius: 50%; animation: spin 1s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
App.jsx
import React from "react";
import Chat from "./components/Chat.jsx";

export default function App() {
  return (
    <div className="app">
      <header className="app__header">
        <div className="logo">AI</div>
        <div className="titles">
          <h1>AI Chatbot</h1>
          <p>Clean UI  Your API key stays on server  Enter to send, Shift+Enter = newline</p>
        </div>
      </header>
      <Chat />
    </div>
  );
}
main.jsx
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.jsx";
import "./styles.css";

ReactDOM.createRoot(document.getElementById("root")).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);
  • src/components
Avatar.jsx
import React from "react";

export default function Avatar({ role }) {
  const base = "avatar";
  if (role === "error") return <div className={`${base} avatar--error`}>!</div>;
  if (role === "user") return <div className={`${base} avatar--user`}>You</div>;
  return <div className={`${base} avatar--ai`}>AI</div>;
}
Chat.jsx
import React, { useEffect, useMemo, useRef, useState } from "react";
import { sendChat } from "../services/api.js";
import MessageBubble from "./MessageBubble.jsx";
import { SendIcon } from "./Icons.jsx";

export default function Chat() {
  const [messages, setMessages] = useState([
    { id: crypto.randomUUID(), role: "assistant", content: "Hi! I’m your AI assistant. Ask me anything. 🧠💬" },
  ]);
  const [input, setInput] = useState("");
  const [model, setModel] = useState("gpt-4.1-mini");
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState("");
  const listRef = useRef(null);

  const canSend = useMemo(() => input.trim().length > 0 && !loading, [input, loading]);

  useEffect(() => {
    const el = listRef.current; if (!el) return; el.scrollTo({ top: el.scrollHeight, behavior: "smooth" });
  }, [messages, loading]);

  async function handleSend() {
    if (!canSend) return;
    setError("");
    const userMsg = { id: crypto.randomUUID(), role: "user", content: input.trim() };
    setMessages(m => [...m, userMsg]);
    setInput("");
    setLoading(true);
    try {
      const payload = messages.filter(m => m.role !== "error").concat(userMsg).map(({ role, content }) => ({ role, content }));
      const { text } = await sendChat({ model, messages: payload });
      setMessages(m => [...m, { id: crypto.randomUUID(), role: "assistant", content: text || "(No response)" }]);
    } catch (e) {
      const msg = e instanceof Error ? e.message : String(e);
      setMessages(m => [...m, { id: crypto.randomUUID(), role: "error", content: `⚠️ Request failed: ${msg}` }]);
      setError(msg);
    } finally {
      setLoading(false);
    }
  }

  function onKeyDown(e) {
    if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSend(); }
  }

  function clearChat() {
    setMessages([{ id: crypto.randomUUID(), role: "assistant", content: "Chat cleared. How can I help now?" }]);
    setError("");
  }

  return (
    <div className="chat">
      <div ref={listRef} className="chat__list">
        {messages.map(m => (<MessageBubble key={m.id} role={m.role} text={m.content} />))}
        {loading && (
          <div className="small" style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
            <div className="spinner" /> Thinking
          </div>
        )}
      </div>

      <div className="composer">
        <div className="composer__inner">
          <div className="box">
            <textarea
              className="input"
              rows={1}
              placeholder="Ask me anything…"
              value={input}
              onChange={(e) => setInput(e.target.value)}
              onKeyDown={onKeyDown}
            />
            <div className="toolbar">
              <div className="small">{error ? "Error — try again" : ""}</div>
              <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
                <select value={model} onChange={(e) => setModel(e.target.value)} className="ghost">
                  <option value="gpt-4.1-mini">gpt-4.1-mini</option>
                  <option value="gpt-4o-mini">gpt-4o-mini</option>
                </select>
                <button className="btn" disabled={!canSend} onClick={handleSend}>
                  <SendIcon /> {loading ? "Sending…" : "Send"}
                </button>
                <button className="btn ghost" onClick={clearChat}>Clear</button>
              </div>
            </div>
          </div>
          <div className="small" style={{ marginTop: 8 }}>Backend: <code>{import.meta.env.VITE_API_BASE || "/api"}</code> • Set <code>OPENAI_API_KEY</code> on the server.</div>
        </div>
      </div>
    </div>
  );
}
CopyButton.jsx
import React, { useState } from "react";

export default function CopyButton({ text }) {
  const [done, setDone] = useState(false);
  async function copy() {
    await navigator.clipboard.writeText(text);
    setDone(true);
    setTimeout(() => setDone(false), 1200);
  }
  return (
    <button className="copy" onClick={copy}>{done ? "Copied" : "Copy"}</button>
  );
}
Icons.jsx
import React from "react";
export function SendIcon() {
  return (
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
      <path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z" />
    </svg>
  );
}
MessageBubble.jsx
import React from "react";
import Avatar from "./Avatar.jsx";
import CopyButton from "./CopyButton.jsx";

export default function MessageBubble({ role, text }) {
  const isUser = role === "user";
  const isError = role === "error";
  return (
    <div className={`message ${isUser ? "message--right" : ""}`}>
      <div className={`bubble ${isUser ? "bubble--user" : isError ? "bubble--error" : "bubble--ai"}`}>
        <div className="bubble__header">
          <Avatar role={role} />
          <span className="meta">{role}</span>
        </div>
        <div>{text}</div>
        {!isError && (
          <div style={{ marginTop: 8, opacity: .8 }}>
            <CopyButton text={text} />
          </div>
        )}
      </div>
    </div>
  );
}
  • src/services
api.js
const API_BASE = import.meta.env.VITE_API_BASE || ""; 

export async function sendChat({ model, messages }) {
  const res = await fetch(`${API_BASE}/api/chat`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ model, messages }),
  });
  if (!res.ok) {
    const text = await res.text();
    throw new Error(text || `HTTP ${res.status}`);
  }
  return res.json();
}
  • src/server
server.js
import express from "express";
import cors from "cors";
import dotenv from "dotenv";
import OpenAI from "openai";

dotenv.config();
const app = express();
app.use(cors());
app.use(express.json());

const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

// Health check
app.get("/api/health", (req, res) => res.json({ ok: true }));

// POST /api/chat { model, messages: [{role, content}, ...] }
app.post("/api/chat", async (req, res) => {
  try {
    const { model = "gpt-4.1-mini", messages = [] } = req.body || {};

    const response = await client.responses.create({
      model,
      input: messages.map(({ role, content }) => ({ role, content })),
    });

    const text = response.output_text ?? "(no output_text)";
    res.json({ text });
  } catch (err) {
    console.error(err);
    res.status(500).json({ error: err?.message || "Unknown error" });
  }
});

const PORT = process.env.PORT || 3001;
app.listen(PORT, () => console.log(`✅ API ready on http://localhost:${PORT}`));
.env
# Rename to .env and add your key
OPENAI_API_KEY=hjd.. //Put your own api key here
PORT=3001
    

Output

Core Files Explained

Entry Point

main.jsx: Renders the root React component (App) into the DOM.

App Component

App.jsx: Sets up the layout and renders the header, titles, and the main Chat component. It provides the structure for the chat interface and contains static content like the app title.

Components

  • Chat.jsx: Handles chat functionality, manages state (messages, loading, error), and sends/receives messages from the server.
  • Avatar.jsx: Displays avatars for user, AI, or error.
  • MessageBubble.jsx: Renders individual chat messages with an option to copy.
  • CopyButton.jsx: Allows the user to copy a specific message to the clipboard.
  • SendIcon.jsx: Displays the send icon used in the chat input form.

API Interaction

api.js: Handles communication with the back-end server. It sends a POST request to the /api/chat route with the chat messages and receives the AI’s response.

Backend (Express)

server.js: Handles two routes:

  • /api/health: Health check to verify server is running.
  • /api/chat: Sends messages to OpenAI API and returns responses.

Environment Variables

.env: Stores the OpenAI API key and server port.


Comment