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 /feedback and 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 (claude or extractive).

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 dev builds and runs it on your machine.

Finally: packaging and deploying the whole stack. 👉