Conectar el agente LangGraph construido en el D1 con una interfaz React funcional. Al finalizar tendrás una app de chat completamente operativa en el navegador, con autenticación, streaming y gestión de estados.
- Backend: la API FastAPI del D1 (agente LangGraph + RAG + PostgreSQL)
- Frontend: app React nueva creada con Vite
# Crear el frontend (una sola vez)
npm create vite@latest frontend -- --template react
cd frontend
npm install axios react-router-domEstructura objetivo:
lab-web-fullstack-with-ai-agent/
├── backend/
│ ├── main.py ← con CORS y endpoint de streaming
│ ├── agent/
│ └── .env
└── frontend/
├── src/
│ ├── api/
│ │ ├── auth.js
│ │ └── client.js
│ ├── context/
│ │ └── AuthContext.jsx
│ ├── components/
│ │ ├── Chat.jsx
│ │ └── ProtectedRoute.jsx
│ ├── pages/
│ │ ├── LoginPage.jsx
│ │ └── ChatPage.jsx
│ └── App.jsx
├── .env ← VITE_API_URL=http://localhost:8000
└── .env.example
En main.py, añade el middleware antes de las rutas:
import os
from fastapi.middleware.cors import CORSMiddleware
ORIGINS = os.getenv("ALLOWED_ORIGINS", "http://localhost:5173").split(",")
app.add_middleware(
CORSMiddleware,
allow_origins=ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)En .env:
ALLOWED_ORIGINS=http://localhost:5173
Verifica que uvicorn main:app --reload arranca y que la ruta /api/chat responde a OPTIONS desde el navegador.
Si tu backend del D1 no tiene autenticación JWT completa, añade este mínimo para poder conectar el frontend:
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
DEMO_TOKEN = os.getenv("DEMO_TOKEN", "demo-token-12345")
security = HTTPBearer()
def get_current_user(creds: HTTPAuthorizationCredentials = Depends(security)):
if creds.credentials != DEMO_TOKEN:
raise HTTPException(status_code=401, detail="Token inválido")
return {"user": "demo"}Añade a .env:
DEMO_TOKEN=demo-token-12345En el frontend, el login simplemente guardará este token estático. Para el proyecto final implementarás JWT real.
Crea frontend/.env:
VITE_API_URL=http://localhost:8000
Crea frontend/.env.example (este sí va al repositorio):
VITE_API_URL=http://localhost:8000
Añade a frontend/.gitignore:
.env
En el código React, accede con import.meta.env.VITE_API_URL.
Implementa los archivos siguiendo las guías de los archivos 2_jwt-react.md y 3_chat-ui-react.md.
src/api/auth.js:
login(email, password)→ llama a/auth/loginy guarda token en localStoragelogout()→ borra el tokengetToken()→ lee el token
src/api/client.js:
axios.create({ baseURL: VITE_API_URL })- Interceptor de request: añade
Authorization: Bearer <token> - Interceptor de response: redirige a
/loginen 401
src/context/AuthContext.jsx:
- Provee
login,logout,isAuth
Añade el endpoint SSE junto al que ya tenías:
from fastapi.responses import StreamingResponse
@app.post("/api/chat/stream")
async def chat_stream(body: ChatInput, user=Depends(get_current_user)):
async def generar():
async for chunk in agente.astream(
{"messages": [{"role": "user", "content": body.message}]},
config={"configurable": {"thread_id": body.session_id}},
):
if "messages" in chunk:
content = chunk["messages"][-1].content
if content:
safe = content.replace("\n", "\\n")
yield f"data: {safe}\n\n"
yield "data: [DONE]\n\n"
return StreamingResponse(
generar(),
media_type="text/event-stream",
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
)Implementa el componente <Chat /> usando fetch con getReader() según 4_streaming-sse.md.
El componente debe:
- Mostrar mensajes del usuario (derecha, azul) y del asistente (izquierda, gris)
- Mostrar el texto del asistente mientras se va generando (chunk a chunk)
- Desactivar el input mientras espera la respuesta
- Gestionar errores de red con un mensaje visible
// App.jsx
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
import { AuthProvider } from "./context/AuthContext";
import { ProtectedRoute } from "./components/ProtectedRoute";
import { LoginPage } from "./pages/LoginPage";
import { ChatPage } from "./pages/ChatPage";
function App() {
return (
<BrowserRouter>
<AuthProvider>
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route
path="/chat"
element={
<ProtectedRoute>
<ChatPage />
</ProtectedRoute>
}
/>
<Route path="*" element={<Navigate to="/chat" />} />
</Routes>
</AuthProvider>
</BrowserRouter>
);
}- Arranca el backend:
uvicorn main:app --reload - Arranca el frontend:
npm run dev(desdefrontend/) - Abre
http://localhost:5173→ debe redirigir a/login - Introduce cualquier email y el
DEMO_TOKENcomo contraseña → redirige a/chat - Envía un mensaje → el texto del asistente aparece token a token
- Apaga el backend → intenta enviar → aparece mensaje de error (no pantalla en blanco)
- Reinicia el backend → el chat vuelve a funcionar
Repositorio con esta estructura:
lab-web-fullstack-with-ai-agent/
├── backend/
│ ├── main.py ← CORS + /api/chat + /api/chat/stream
│ ├── .env.example ← variables de entorno documentadas (sin valores reales)
│ └── requirements.txt
└── frontend/
├── src/
├── .env.example ← VITE_API_URL=http://localhost:8000
└── package.json
-
POST /api/chatacepta peticiones desdelocalhost:5173(sin error CORS) - El login guarda un token y redirige al chat
- Las rutas protegidas redirigen a
/loginsi no hay token - El chat envía mensajes y muestra la respuesta del agente LangGraph
- Las respuestas se muestran en streaming (texto progresivo, no todo de golpe)
- El agente recuerda el contexto dentro de la misma sesión
- Si el backend está caído, el frontend muestra un error (no se rompe en blanco)
- Las API keys y tokens NO están en el código fuente (
.enven.gitignore)
- Añade un selector de
session_idpara que el usuario pueda tener varias conversaciones - Muestra el historial de conversaciones al volver a cargar la página (recuperar de
/api/chat/history) - Añade un botón "Nueva conversación" que limpie el estado y genere un nuevo
session_id
