05. Frontend Framework
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
}
Build the upload component with drag-and-drop, progress bar, and error handling for files over 50MB.