RUNLOCALAIv38
->Will it run?Best GPUCompareTroubleshootStartLearnPulseModelsHardwareToolsBench
Run check
RUNLOCALAI

Independently operated catalog for local-AI hardware and software. Hand-written verdicts. Source-cited claims. Reproducible commands when we have them.

OP·Fredoline Eruo
DIR
  • Models
  • Hardware
  • Tools
  • Benchmarks
TOOLS
  • Will it run?
  • Compare hardware
  • Cost vs cloud
  • Choose my GPU
  • Prompting kits
  • Quick answers
REF
  • All buyer guides
  • Learn local AI
  • Methodology
  • Glossary
  • Errors KB
  • Trust
EDITOR
  • About
  • Author
  • How we make money
  • Editorial policy
  • Contact
LEGAL
  • Privacy
  • Terms
  • Sitemap
MAIL · MONTHLY DIGEST
Get monthly local AI changes
Monthly recap. No spam.
DISCLOSURE

Some links on this site are affiliate links (Amazon Associates and other first-class retailers). When you buy through them, we earn a small commission at no extra cost to you. Affiliate links do not influence our verdicts — there are cards we rate highly that we don't have affiliate relationships with, and cards that sell well that we refuse to recommend. Read more →

© 2026 runlocalai.coIndependently operated
RUNLOCALAI · v38
  1. >
  2. Home
  3. /Learn
  4. /Courses
  5. /Capstone: Full-Stack AI App
  6. /Ch. 5
Capstone: Full-Stack AI App

05. Frontend Framework

Chapter 5 of 18 · 20 min
KEY INSIGHT

Streaming UI requires careful state management—buffer chunks locally and batch renders to avoid excessive re-renders.

The frontend uses React with TypeScript for type safety across the API boundary. Vite provides fast development builds and optimized production bundles. The architecture separates concerns: components handle rendering, hooks manage state, and services communicate with the backend.

Project structure follows feature-based organization:

src/
├── features/
│   ├── auth/
│   │   ├── components/
│   │   ├── hooks/
│   │   └── services/
│   ├── documents/
│   │   ├── components/
│   │   ├── hooks/
│   │   └── services/
│   └── chat/
│       ├── components/
│       ├── hooks/
│       └── services/
├── shared/
│   ├── components/
│   ├── hooks/
│   └── types/
└── App.tsx

The chat feature uses Server-Sent Events for streaming responses. The useChat hook manages the EventSource connection and updates state incrementally:

// features/chat/hooks/useChat.ts
import { useState, useCallback } from 'react';
import { ChatMessage } from '../../shared/types';

export function useChat() {
  const [messages, setMessages] = useState<ChatMessage[]>([]);
  const [isStreaming, setIsStreaming] = useState(false);

  const sendMessage = useCallback(async (content: string, documentId: string) => {
    setIsStreaming(true);
    setMessages(prev => [...prev, { role: 'user', content }]);

    const response = await fetch('/api/v1/ask', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ question: content, document_id: documentId }),
    });

    const reader = response.body?.getReader();
    const decoder = new TextDecoder();

    if (!reader) {
      setIsStreaming(false);
      return;
    }

    let assistantMessage = '';
    setMessages(prev => [...prev, { role: 'assistant', content: '' }]);

    while (true) {
      const { done, value } = await reader.read();
      if (done) break;

      const chunk = decoder.decode(value);
      assistantMessage += chunk;

      setMessages(prev => {
        const updated = [...prev];
        updated[updated.length - 1] = { role: 'assistant', content: assistantMessage };
        return updated;
      });
    }

    setIsStreaming(false);
  }, []);

  return { messages, sendMessage, isStreaming };
}

Error handling requires attention. Network failures should show retry buttons. Timeout errors (30 seconds for streaming) should display user-friendly messages. Authentication errors should redirect to login.

The upload component uses a dropzone with progress tracking:

// features/documents/components/UploadZone.tsx
import { useCallback, useState } from 'react';
import { Upload, File } from 'lucide-react';

interface UploadZoneProps {
  onUploadComplete: (documentId: string) => void;
}

export function UploadZone({ onUploadComplete }: UploadZoneProps) {
  const [uploading, setUploading] = useState(false);
  const [progress, setProgress] = useState(0);

  const handleUpload = useCallback(async (files: FileList) => {
    const file = files[0];
    if (!file) return;

    setUploading(true);
    const formData = new FormData();
    formData.append('file', file);

    const xhr = new XMLHttpRequest();
    xhr.upload.addEventListener('progress', (e) => {
      if (e.lengthComputable) {
        setProgress((e.loaded / e.total) * 100);
      }
    });

    xhr.addEventListener('load', () => {
      if (xhr.status === 200) {
        const { document_id } = JSON.parse(xhr.responseText);
        onUploadComplete(document_id);
      }
      setUploading(false);
    });

    xhr.open('POST', '/api/v1/upload');
    xhr.send(formData);
  }, [onUploadComplete]);

  // Render dropzone UI with file icon, progress bar, and styling
}
EXERCISE

Build the upload component with drag-and-drop, progress bar, and error handling for files over 50MB.

← Chapter 4
API Gateway
Chapter 6 →
UI/UX Design