DS DevShelfHub Projects · AI tools
Tutorials / RAG & Vector DBs / Frontend Integration
RAG Pipeline Intermediate · 13 min read Page 18 of 23

Frontend Integration

By DevShelfHub

Chat UI patterns, streaming responses, source attribution, confidence scores, and feedback loops.

Series progress18 / 23
RAG Frontend Integration — RAG pipeline tutorial

Chat UI Patterns

Python
// React Chat Component
function ChatUI() {
  const [messages, setMessages] = useState([]);
  const [input, setInput] = useState('');

  const handleSend = async () => {
    // Add user message
    setMessages(prev => [...prev, { role: 'user', content: input }]);

    // Stream response
    const response = await fetch('/api/query', {
      method: 'POST',
      body: JSON.stringify({ query: input }),
      headers: { 'Content-Type': 'application/json' }
    });

    const reader = response.body.getReader();
    let assistantMessage = '';

    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      assistantMessage += new TextDecoder().decode(value);
      setMessages(prev => [
        ...prev.slice(0, -1),
        { role: 'assistant', content: assistantMessage }
      ]);
    }
  };

  return (
    <div className="chat">
      {messages.map((msg, i) => (
        <div key={i} className={msg.role}>
          {msg.content}
        </div>
      ))}
      <input onChange={e => setInput(e.target.value)} />
      <button onClick={handleSend}>Send</button>
    </div>
  );
}

Streaming Responses

Server-Sent Events (SSE): Stream text chunks as they generate.

Python
// Server (Python)
@app.post("/api/query")
async def stream_query(query: str):
    async def generate():
        # Retrieve docs
        docs = await retrieve(query)

        # Stream LLM response
        async for chunk in llm.stream(query, docs):
            yield f"data: {json.dumps({'text': chunk})}\n\n"

    return StreamingResponse(generate(), media_type="text/event-stream")

// Client (JS)
const eventSource = new EventSource('/api/query?q=question');
eventSource.onmessage = (event) => {
  const { text } = JSON.parse(event.data);
  document.getElementById('response').textContent += text;
};

Benefit: User sees answer immediately, doesn't wait for full LLM response.

Source Attribution & Trust

YAML
// Show retrieved sources
const response = {
  answer: "Self-employed people must file quarterly...",
  sources: [
    {
      title: "Tax Guide 2024",
      url: "https://...",
      excerpt: "Self-employed people must...",
      relevance: 0.95
    },
    {
      title: "IRS FAQ",
      url: "https://...",
      excerpt: "Estimated taxes are...",
      relevance: 0.82
    }
  ],
  confidence: 0.92
};

// UI
<div className="sources">
  <h3>Sources ({response.sources.length})</h3>
  {response.sources.map(src => (
    <a key={src.title} href={src.url}>
      {src.title}
      <span className="relevance">{Math.round(src.relevance * 100)}%</span>
    </a>
  ))}
  <span className="confidence">Answer confidence: {Math.round(response.confidence * 100)}%</span>
</div>

Trust building: Show where information came from, confidence score, relevance percentages.

Feedback Loop

Python
// Feedback buttons
<div className="feedback">
  <button onClick={() => submitFeedback('helpful')}>👍 Helpful</button>
  <button onClick={() => submitFeedback('not_helpful')}>👎 Not helpful</button>
  <textarea placeholder="Comments..." />
</div>

// Server logs feedback for improvement
async def submit_feedback(query_id, feedback, comments):
    db.insert({
        "query_id": query_id,
        "feedback": feedback,
        "comments": comments,
        "timestamp": time.time()
    })
    # Use feedback to retrain/improve RAG

Use feedback to: identify bad answers, retrain embeddings, improve prompts.

Notes

SSE is almost always the right streaming choice

Server-Sent Events work over plain HTTP/1.1, survive proxy re-buffering (with X-Accel-Buffering: no), and reconnect automatically. WebSockets add bidirectional complexity that RAG streaming doesn't need — you're always pushing tokens from server to client. Use WebSockets only if you need low-latency user interruptions (e.g., a stop-generation button that halts the LLM call server-side).

Send source citations as a separate event, not inline

Mixing citation metadata into the streamed token stream forces the client to parse structured data out of natural-language text — fragile and slow. Instead, stream the answer tokens first, then emit a final sources SSE event with a JSON array of {title, url, snippet} objects. The UI renders them only after the answer is complete, matching user expectations.

Loading states prevent users from re-submitting

RAG queries take 1–4 seconds end-to-end. Without explicit loading feedback, users click the submit button again, triggering duplicate requests that can cause rate-limit errors or doubled responses. Disable the submit button immediately on click, show a skeleton or spinner, and re-enable only after the full response (including sources) arrives.

Persist user feedback before the page unloads

Thumbs-up/down feedback submitted just before navigation (back button, new tab) can be silently lost if the POST request is cancelled. Use navigator.sendBeacon() for feedback submissions — it queues the payload to send even as the page is unloading, ensuring no evaluation signal is dropped.

RAG Frontend Integration FAQ

How do I stream RAG responses to the browser?

Use server-sent events (SSE) or WebSockets. The backend streams LLM output tokens as they arrive; the frontend appends each token to the UI. FastAPI and Django Channels both support streaming HTTP responses.

How do I display RAG source citations in a UI?

Return retrieved chunk metadata (source URL, page number, document title) alongside the answer. Render them as footnote links or a collapsible "Sources" panel. Let users click through to verify the answer.

How do I build a feedback loop for RAG quality improvement?

Add thumbs-up/thumbs-down buttons after each answer. Log the query, retrieved chunks, answer, and user vote to a database. Use negative examples to identify retrieval failures and retune your chunking or embedding strategy.

Should I show retrieved chunks to users in a RAG UI?

Showing highlighted excerpts from source documents significantly increases user trust. Implement a "See sources" toggle that reveals the top-3 retrieved chunks with the relevant passage highlighted.

What frontend frameworks work well for RAG chatbots?

React with Vercel AI SDK (useChat hook) or Next.js with streaming support are the most common choices. For internal tools, Streamlit and Gradio let you build a working RAG UI in under 50 lines of Python.