Haan. Main tumhe complete working mini ChatGPT de raha hoon:
Frontend: React + Vite
Backend: FastAPI
LLM: OpenAI Responses API
Streaming: Backend → React real-time chunks
Chat history: frontend se backend ko poori conversation
Stop button:
AbortControllerCORS
.envProper error handling
OpenAI ki current API mein text generation ke liye Responses API recommended path hai, aur Python SDK available hai. (OpenAI Platform)
1. Project structure
streaming-chatbot/
│
├── backend/
│ ├── main.py
│ ├── requirements.txt
│ ├── .env
│ └── .gitignore
│
└── frontend/
├── package.json
├── index.html
└── src/
├── App.jsx
├── main.jsx
└── index.css2. Backend — FastAPI
backend/requirements.txt
fastapi
uvicorn[standard]
openai
python-dotenvInstall:
cd backend
python -m venv venvWindows:
venv\Scripts\activateMac/Linux:
source venv/bin/activateThen:
pip install -r requirements.txtbackend/.env
OPENAI_API_KEY=your_openai_api_key_hereAPI key frontend mein kabhi mat rakhna. Sirf backend .env mein rakho.
backend/.gitignore
venv/
__pycache__/
.env3. Backend main.py
import os
import json
from dotenv import load_dotenv
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from openai import OpenAI
# --------------------------------------------------
# Environment
# --------------------------------------------------
load_dotenv()
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
if not OPENAI_API_KEY:
raise RuntimeError("OPENAI_API_KEY is not set")
client = OpenAI(api_key=OPENAI_API_KEY)
# --------------------------------------------------
# FastAPI app
# --------------------------------------------------
app = FastAPI()
# --------------------------------------------------
# CORS
# --------------------------------------------------
app.add_middleware(
CORSMiddleware,
allow_origins=[
"http://localhost:5173",
],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# --------------------------------------------------
# Request models
# --------------------------------------------------
class Message(BaseModel):
role: str
content: str
class ChatRequest(BaseModel):
messages: list[Message]
# --------------------------------------------------
# Health check
# --------------------------------------------------
@app.get("/")
async def root():
return {
"message": "Streaming chatbot backend is running"
}
# --------------------------------------------------
# Streaming generator
# --------------------------------------------------
def generate_response(messages: list[Message]):
"""
Calls OpenAI and streams text chunks
back to the React frontend.
"""
input_messages = []
for message in messages:
input_messages.append(
{
"role": message.role,
"content": message.content,
}
)
try:
stream = client.responses.create(
model="gpt-5.6",
input=input_messages,
stream=True,
)
for event in stream:
# We only care about text delta events.
if event.type == "response.output_text.delta":
data = {
"type": "text",
"content": event.delta,
}
yield f"data: {json.dumps(data)}\n\n"
# Response completed
elif event.type == "response.completed":
data = {
"type": "done",
}
yield f"data: {json.dumps(data)}\n\n"
except Exception as e:
print("OpenAI error:", e)
data = {
"type": "error",
"message": "Something went wrong while generating the response.",
}
yield f"data: {json.dumps(data)}\n\n"
# --------------------------------------------------
# Chat endpoint
# --------------------------------------------------
@app.post("/api/chat")
async def chat(request: ChatRequest):
return StreamingResponse(
generate_response(request.messages),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no",
},
)Backend run karo
uvicorn main:app --reloadBackend:
http://localhost:8000Browser mein:
http://localhost:8000open karoge toh:
{
"message": "Streaming chatbot backend is running"
}dikhega.
4. Frontend create karo
Root directory mein:
npm create vite@latest frontend -- --template reactThen:
cd frontend
npm install5. frontend/src/App.jsx
import { useRef, useState } from "react";
function App() {
const [messages, setMessages] = useState([]);
const [input, setInput] = useState("");
const [isLoading, setIsLoading] = useState(false);
const abortControllerRef = useRef(null);
// ----------------------------------------------
// Send message
// ----------------------------------------------
const sendMessage = async () => {
const trimmedInput = input.trim();
if (!trimmedInput || isLoading) {
return;
}
// --------------------------------------------
// Create user message
// --------------------------------------------
const userMessage = {
id: crypto.randomUUID(),
role: "user",
content: trimmedInput,
};
// Add user message immediately
setMessages((prev) => [...prev, userMessage]);
setInput("");
setIsLoading(true);
// --------------------------------------------
// Create empty assistant message
// --------------------------------------------
const assistantMessage = {
id: crypto.randomUUID(),
role: "assistant",
content: "",
};
setMessages((prev) => [
...prev,
assistantMessage,
]);
// --------------------------------------------
// AbortController
// --------------------------------------------
const controller = new AbortController();
abortControllerRef.current = controller;
try {
// ------------------------------------------
// IMPORTANT:
// Send previous messages + new user message
// ------------------------------------------
const messagesForBackend = [
...messages,
userMessage,
].map((message) => ({
role: message.role,
content: message.content,
}));
// ------------------------------------------
// Request
// ------------------------------------------
const response = await fetch(
"http://localhost:8000/api/chat",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
messages: messagesForBackend,
}),
signal: controller.signal,
}
);
if (!response.ok) {
throw new Error(
`HTTP error: ${response.status}`
);
}
if (!response.body) {
throw new Error(
"Streaming is not supported by this browser."
);
}
// ------------------------------------------
// Read stream
// ------------------------------------------
const reader =
response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { value, done } =
await reader.read();
if (done) {
break;
}
buffer += decoder.decode(value, {
stream: true,
});
// ----------------------------------------
// SSE messages are separated by \n\n
// ----------------------------------------
const events = buffer.split("\n\n");
buffer = events.pop() || "";
for (const event of events) {
if (!event.startsWith("data:")) {
continue;
}
const jsonString = event
.replace(/^data:\s*/, "")
.trim();
if (!jsonString) {
continue;
}
const data = JSON.parse(jsonString);
// --------------------------------------
// Text chunk
// --------------------------------------
if (data.type === "text") {
setMessages((prevMessages) =>
prevMessages.map((message) => {
if (
message.id ===
assistantMessage.id
) {
return {
...message,
content:
message.content +
data.content,
};
}
return message;
})
);
}
// --------------------------------------
// Error
// --------------------------------------
if (data.type === "error") {
setMessages((prevMessages) =>
prevMessages.map((message) => {
if (
message.id ===
assistantMessage.id
) {
return {
...message,
content:
"❌ " + data.message,
};
}
return message;
})
);
}
// --------------------------------------
// Done
// --------------------------------------
if (data.type === "done") {
console.log(
"Generation completed"
);
}
}
}
} catch (error) {
// Abort is expected when user clicks Stop
if (error.name === "AbortError") {
console.log("Generation stopped");
} else {
console.error(error);
setMessages((prevMessages) =>
prevMessages.map((message) => {
if (
message.id ===
assistantMessage.id
) {
return {
...message,
content:
"❌ Failed to get response.",
};
}
return message;
})
);
}
} finally {
setIsLoading(false);
abortControllerRef.current = null;
}
};
// ----------------------------------------------
// Stop generation
// ----------------------------------------------
const stopGeneration = () => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
};
// ----------------------------------------------
// Enter key
// ----------------------------------------------
const handleKeyDown = (event) => {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
sendMessage();
}
};
// ----------------------------------------------
// UI
// ----------------------------------------------
return (
<div className="app">
<header className="header">
<h1>AI Chat</h1>
<span className="status">
{isLoading
? "Generating..."
: "Ready"}
</span>
</header>
{/* -----------------------------------------
Messages
------------------------------------------ */}
<main className="chat-container">
{messages.length === 0 && (
<div className="empty-state">
<h2>How can I help you?</h2>
<p>
Ask me anything and watch the
response stream in real time.
</p>
</div>
)}
{messages.map((message) => (
<div
key={message.id}
className={`message-row ${message.role}`}
>
<div className="avatar">
{message.role === "user"
? "U"
: "AI"}
</div>
<div className="message-content">
{message.content}
{/* Cursor while streaming */}
{isLoading &&
message.role === "assistant" &&
message.id ===
messages[messages.length - 1]
?.id && (
<span className="cursor">
▌
</span>
)}
</div>
</div>
))}
</main>
{/* -----------------------------------------
Input
------------------------------------------ */}
<div className="input-area">
<div className="input-wrapper">
<textarea
value={input}
onChange={(event) =>
setInput(event.target.value)
}
onKeyDown={handleKeyDown}
placeholder="Message AI..."
rows={1}
disabled={isLoading}
/>
{!isLoading ? (
<button
onClick={sendMessage}
disabled={!input.trim()}
className="send-button"
>
Send
</button>
) : (
<button
onClick={stopGeneration}
className="stop-button"
>
Stop
</button>
)}
</div>
<p className="hint">
Enter to send · Shift + Enter for new line
</p>
</div>
</div>
);
}
export default App;6. frontend/src/index.css
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family:
Inter,
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
sans-serif;
background: #ffffff;
color: #171717;
}
button,
textarea {
font: inherit;
}
/* -----------------------------------------------
App
------------------------------------------------ */
.app {
min-height: 100vh;
display: flex;
flex-direction: column;
background: #ffffff;
}
/* -----------------------------------------------
Header
------------------------------------------------ */
.header {
height: 64px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24px;
border-bottom: 1px solid #e5e5e5;
background: #ffffff;
}
.header h1 {
margin: 0;
font-size: 20px;
font-weight: 600;
}
.status {
font-size: 13px;
color: #737373;
}
/* -----------------------------------------------
Chat
------------------------------------------------ */
.chat-container {
width: 100%;
max-width: 850px;
flex: 1;
margin: 0 auto;
padding: 40px 24px 160px;
}
/* -----------------------------------------------
Empty state
------------------------------------------------ */
.empty-state {
text-align: center;
margin-top: 120px;
}
.empty-state h2 {
margin-bottom: 8px;
font-size: 28px;
}
.empty-state p {
color: #737373;
}
/* -----------------------------------------------
Message
------------------------------------------------ */
.message-row {
display: flex;
gap: 14px;
margin-bottom: 28px;
line-height: 1.6;
}
.message-row.user {
flex-direction: row-reverse;
}
.avatar {
flex-shrink: 0;
width: 34px;
height: 34px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: #f0f0f0;
font-size: 11px;
font-weight: 600;
}
.message-row.user .avatar {
background: #171717;
color: white;
}
.message-content {
max-width: 75%;
padding: 10px 14px;
border-radius: 14px;
white-space: pre-wrap;
word-break: break-word;
}
.message-row.user .message-content {
background: #f0f0f0;
}
.message-row.assistant .message-content {
padding-left: 0;
}
/* -----------------------------------------------
Streaming cursor
------------------------------------------------ */
.cursor {
display: inline-block;
margin-left: 2px;
animation: blink 0.8s infinite;
}
@keyframes blink {
0% {
opacity: 1;
}
50% {
opacity: 0;
}
100% {
opacity: 1;
}
}
/* -----------------------------------------------
Input area
------------------------------------------------ */
.input-area {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 20px;
background: linear-gradient(
transparent,
white 25%
);
}
.input-wrapper {
width: 100%;
max-width: 800px;
margin: 0 auto;
display: flex;
align-items: flex-end;
gap: 10px;
padding: 10px;
border: 1px solid #d4d4d4;
border-radius: 16px;
background: white;
box-shadow:
0 4px 20px rgba(0, 0, 0, 0.06);
}
textarea {
flex: 1;
resize: none;
border: none;
outline: none;
padding: 10px;
min-height: 42px;
max-height: 160px;
background: transparent;
color: #171717;
}
textarea::placeholder {
color: #a3a3a3;
}
/* -----------------------------------------------
Buttons
------------------------------------------------ */
.send-button,
.stop-button {
border: none;
border-radius: 10px;
padding: 10px 16px;
cursor: pointer;
font-weight: 500;
}
.send-button {
background: #171717;
color: white;
}
.send-button:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.stop-button {
background: #eeeeee;
color: #171717;
}
/* -----------------------------------------------
Hint
------------------------------------------------ */
.hint {
width: 100%;
max-width: 800px;
margin: 8px auto 0;
text-align: center;
font-size: 12px;
color: #a3a3a3;
}
/* -----------------------------------------------
Mobile
------------------------------------------------ */
@media (max-width: 600px) {
.chat-container {
padding: 24px 16px 150px;
}
.header {
padding: 0 16px;
}
.message-content {
max-width: 85%;
}
.input-area {
padding: 12px;
}
}7. frontend/src/main.jsx
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import "./index.css";
ReactDOM.createRoot(
document.getElementById("root")
).render(
<React.StrictMode>
<App />
</React.StrictMode>
);8. Frontend run karo
cd frontend
npm run devOpen:
http://localhost:5173Ab architecture ye hai:
┌─────────────────┐
│ React UI │
│ │
│ User types msg │
└────────┬────────┘
│
│ POST /api/chat
│
▼
┌─────────────────┐
│ FastAPI │
│ │
│ /api/chat │
└────────┬────────┘
│
│ streaming=True
▼
┌─────────────────┐
│ OpenAI API │
│ │
│ Model response │
└────────┬────────┘
│
│ text delta
▼
┌─────────────────┐
│ FastAPI │
│ │
│ yield SSE chunk │
└────────┬────────┘
│
│ data: {...}
│
▼
┌─────────────────┐
│ React │
│ │
│ reader.read() │
│ ↓ │
│ append chunk │
└─────────────────┘Sabse important streaming part
Backend:
stream = client.responses.create(
model="gpt-5.6",
input=input_messages,
stream=True,
)
for event in stream:
if event.type == "response.output_text.delta":
yield f"data: {json.dumps({
'type': 'text',
'content': event.delta
})}\n\n"Frontend:
const reader = response.body.getReader();
while (true) {
const { value, done } = await reader.read();
if (done) break;
// chunk mil gaya
}Yahi actual streaming hai — response complete hone ka wait nahi ho raha. Model jo text generate karta ja raha hai, backend usko SSE event ke through forward karta ja raha hai aur React usko immediately UI mein append karta ja raha hai. OpenAI ki current Responses API documentation bhi streaming responses ko support karti hai. (OpenAI Platform)
Ek important production point
Is demo mein conversation history React state mein hai. Production app mein history ko backend/database mein persist karna better hoga, aur authentication, rate limiting, request validation, logging, token limits, etc. add karne chahiye.
Agar tum isko real ChatGPT clone jaisa banana chahte ho, next logical upgrade hai: Markdown + code syntax highlighting + auto-scroll + regenerate + edit message + conversation DB + authentication + streaming cancellation on backend.
Comments
Post a Comment