The React frontend
A recommender needs a face. The capstone ships a small React (Vite) app that talks to the FastAPI backend and gives you three panels: recommendations, search, and the RAG assistant.
What it does
- Recommendations — enter a user id (e.g.
U106), see their top articles; a badge shows when a user is cold start (falling back to trending). Clicking a card sends/feedbackand refreshes, so you can watch recommendations adapt in real time. - Search — type a query, get vector-search results with similarity scores.
- Ask — ask a question, get a RAG answer with cited sources and the mode
(
claudeorextractive).
The API client
A thin wrapper around fetch. In dev, Vite proxies /api/* to the backend on
:8000 (configured in vite.config.js), so there are no CORS issues.
// Thin client for the FastAPI backend. In dev, vite proxies /api -> :8000.
const BASE = import.meta.env.VITE_API_BASE || '/api'
async function get(path) {
const r = await fetch(`${BASE}${path}`)
if (!r.ok) throw new Error(`${r.status} ${r.statusText}`)
return r.json()
}
async function post(path, body) {
const r = await fetch(`${BASE}${path}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
if (!r.ok) throw new Error(`${r.status} ${r.statusText}`)
return r.json()
}
export const api = {
health: () => get('/health'),
recommend: (userId, k = 10) => get(`/recommend/${encodeURIComponent(userId)}?k=${k}`),
search: (q, k = 10) => get(`/search?q=${encodeURIComponent(q)}&k=${k}`),
ask: (query, k = 5) => post('/ask', { query, k }),
feedback: (userId, newsId) => post('/feedback', { user_id: userId, news_id: newsId, event: 'click' }),
}
The app
One component, three sections, plain React hooks — easy to read and extend.
import React, { useEffect, useState } from 'react'
import { api } from './api.js'
function Card({ a, onClick }) {
return (
<div className="card" onClick={onClick}>
<span className={`tag tag-${a.subcategory}`}>{a.subcategory}</span>
<h4>{a.title}</h4>
<p>{a.abstract}</p>
{a.score !== undefined && <small>score {a.score}</small>}
</div>
)
}
export default function App() {
const [health, setHealth] = useState(null)
const [userId, setUserId] = useState('U106')
const [recs, setRecs] = useState([])
const [cold, setCold] = useState(false)
const [query, setQuery] = useState('world cup final')
const [results, setResults] = useState([])
const [question, setQuestion] = useState('Who won the Champions League?')
const [answer, setAnswer] = useState(null)
const [err, setErr] = useState(null)
useEffect(() => { api.health().then(setHealth).catch(e => setErr(String(e))) }, [])
async function loadRecs() {
try { const r = await api.recommend(userId, 8); setRecs(r.recommendations); setCold(r.cold_start) }
catch (e) { setErr(String(e)) }
}
useEffect(() => { loadRecs() }, [])
async function doSearch() {
try { setResults((await api.search(query, 6)).results) } catch (e) { setErr(String(e)) }
}
async function doAsk() {
try { setAnswer(await api.ask(question, 4)) } catch (e) { setErr(String(e)) }
}
async function click(newsId) {
await api.feedback(userId, newsId); loadRecs() // online update: recs react
}
return (
<div className="app">
<header>
<h1>📰 News Recommender</h1>
{health && <span className="status">
{health.articles} articles · {health.embedder} · RAG: {health.rag_mode}
</span>}
</header>
{err && <div className="error">Backend error: {err} — is the API running on :8000?</div>}
<section>
<h2>Recommendations</h2>
<div className="row">
<input value={userId} onChange={e => setUserId(e.target.value)} placeholder="user id (e.g. U106)" />
<button onClick={loadRecs}>Load</button>
{cold && <span className="cold">cold start → trending</span>}
</div>
<div className="grid">
{recs.map(a => <Card key={a.id} a={a} onClick={() => click(a.id)} />)}
</div>
<small>Tip: clicking a card sends feedback and refreshes — recommendations adapt.</small>
</section>
<section>
<h2>Search</h2>
<div className="row">
<input value={query} onChange={e => setQuery(e.target.value)} />
<button onClick={doSearch}>Search</button>
</div>
<div className="grid">{results.map(a => <Card key={a.id} a={a} />)}</div>
</section>
<section>
<h2>Ask the news assistant (RAG)</h2>
<div className="row">
<input value={question} onChange={e => setQuestion(e.target.value)} />
<button onClick={doAsk}>Ask</button>
</div>
{answer && (
<div className="answer">
<pre>{answer.answer}</pre>
<div className="sources">
{answer.sources.map((s, i) => <span key={s.id}>[{i + 1}] {s.title}</span>)}
</div>
<small>mode: {answer.mode}</small>
</div>
)}
</section>
</div>
)
}
Running it
The frontend needs the backend from Chapter 21 running on
:8000, then:
cd frontend
npm install
npm run dev # opens http://localhost:5173
That's it — a working recommender UI: browse personalized news, search by meaning, and chat with the news assistant.
Project layout
frontend/
├── index.html
├── package.json # react + vite
├── vite.config.js # dev proxy /api -> :8000
└── src/
├── main.jsx # React entry
├── api.js # fetch wrapper for the backend
├── App.jsx # the 3-panel UI
└── styles.css # dark theme
Production build & deploy
npm run build # outputs static files to frontend/dist/
Serve dist/ from any static host (S3 + CloudFront, Nginx, Vercel, Netlify…),
point VITE_API_BASE at your deployed API, and you have a deployable UI. The
Docker Compose setup runs the dev server alongside the
backend so you can try the whole stack with one command.
Note. This box can't run a Node build, so the React app is provided as complete, standard code (verified by review, not executed here).
npm install && npm run devbuilds and runs it on your machine.
Finally: packaging and deploying the whole stack. 👉