Frontend Architecture

This document describes the architecture of the Frontend service, including component structure, data flow patterns, and design decisions.

High-Level Architecture

graph TB
    subgraph "Browser"
        subgraph "React Application"
            Router[React Router]
            
            subgraph "State Management"
                AuthCtx[AuthContext]
                QueryClient[TanStack Query Client]
            end
            
            subgraph "Route Guards"
                RA[RequireAuth]
                RS[RequireStudent]
                RP[RequireProfessor]
                RAd[RequireAdmin]
            end
            
            subgraph "Pages"
                Auth[Auth Pages]
                Chat[Chat Page]
                Dashboard[Dashboard Page]
                Admin[Admin Dashboard]
                Settings[Settings Page]
            end
            
            subgraph "Hooks Layer"
                ChatHooks[Chat Hooks]
                SessionHooks[Session Hooks]
                DashHooks[Dashboard Hooks]
                AdminHooks[Admin Hooks]
            end
            
            subgraph "API Layer"
                Axios[Axios Instance]
            end
        end
    end
    
    Backend[Backend API :8000]
    
    Router --> RA
    Router --> RS
    Router --> RP
    Router --> RAd
    
    RA --> Auth
    RS --> Chat
    RP --> Dashboard
    RAd --> Admin
    
    Auth --> AuthCtx
    Chat --> ChatHooks
    Chat --> SessionHooks
    Dashboard --> DashHooks
    Admin --> AdminHooks
    
    ChatHooks --> QueryClient
    SessionHooks --> QueryClient
    DashHooks --> QueryClient
    AdminHooks --> QueryClient
    
    QueryClient --> Axios
    AuthCtx --> Axios
    Axios --> Backend

Design Principles

1. Component-Driven Architecture

The application follows a component-driven approach with clear separation of concerns:

  • Pages - Route-level components that compose features
  • Components - Reusable UI elements organized by domain
  • Hooks - Business logic extracted into reusable hooks
  • Context - Global state shared across the component tree

2. Colocation

Related code is colocated by feature:

pages/
├── chat/
│   └── ChatPage.tsx          # Chat feature page
├── dashboard/
│   └── DashboardPage.tsx     # Professor dashboard
└── admin/
    └── AdminDashboard.tsx    # Admin panel

components/
├── chat/                     # Chat-specific components
├── dashboard/                # Dashboard-specific components
└── ui/                       # Shared UI primitives

3. Type Safety

TypeScript is used throughout with strict mode enabled:

// Types are defined in dedicated files
// src/types/chat.ts
export interface Message {
  id: string;
  role: "user" | "assistant";
  content: string;
  timestamp: Date;
}

export interface Session {
  id: string;
  title: string;
  asignatura: string;
  created_at: string;
  updated_at: string;
}

Data Flow

Authentication Flow

sequenceDiagram
    participant U as User
    participant LP as LoginPage
    participant API as api.ts
    participant AC as AuthContext
    participant LS as localStorage
    participant BE as Backend

    U->>LP: Enter credentials
    LP->>API: POST /token (form data)
    API->>BE: OAuth2 password grant
    BE-->>API: { access_token }
    API->>BE: GET /users/me
    BE-->>API: { user details }
    API-->>LP: token + user
    LP->>AC: login(token, user)
    AC->>LS: Store token
    AC->>AC: Set user state
    LP->>LP: Navigate to /chat

Chat Message Flow

sequenceDiagram
    participant U as User
    participant CI as ChatInput
    participant UC as useChat
    participant TQ as TanStack Query
    participant API as Axios
    participant BE as Backend
    participant CB as Chatbot

    U->>CI: Send message
    CI->>UC: sendMessage(text)
    UC->>UC: Add optimistic user message
    UC->>API: POST /chat (session_id, message)
    API->>BE: Forward request
    BE->>CB: Route to chatbot
    CB-->>BE: AI response
    BE-->>API: { response }
    API-->>UC: Message received
    UC->>UC: Add assistant message
    UC->>TQ: Invalidate session queries

Data Fetching Pattern

graph TB
    subgraph "Component"
        Comp[React Component]
    end
    
    subgraph "Hook Layer"
        Hook[Custom Hook]
        TQ[TanStack Query]
    end
    
    subgraph "API Layer"
        Axios[Axios Instance]
        Interceptor[Auth Interceptor]
    end
    
    subgraph "Backend"
        BE[Backend API]
    end
    
    Comp -->|calls| Hook
    Hook -->|useQuery/useMutation| TQ
    TQ -->|fetches via| Axios
    Axios -->|adds token| Interceptor
    Interceptor -->|requests| BE
    BE -->|response| Axios
    Axios -->|caches| TQ
    TQ -->|updates| Comp

Layer Responsibilities

Pages Layer

Pages are route-level components that:

  • Compose multiple components
  • Handle page-level state
  • Coordinate between features
  • Define page layout
// Example: ChatPage.tsx
export default function ChatPage() {
  const { user } = useAuth();
  const { data: sessions } = useSessions();
  const [activeSessionId, setActiveSessionId] = useState<string | null>(null);
  
  // Compose components
  return (
    <div className="flex h-full">
      <SessionSelector sessions={sessions} onSelect={setActiveSessionId} />
      <ChatInterface sessionId={activeSessionId} subject={user?.subjects[0]} />
    </div>
  );
}

Hooks Layer

Custom hooks encapsulate:

  • API calls via TanStack Query
  • Business logic
  • State transformations
  • Side effects
// Example: useChat.ts
export function useChat(sessionId: string | null) {
  const [messages, setMessages] = useState<Message[]>([]);
  const [isLoading, setIsLoading] = useState(false);

  const sendMessage = async (content: string) => {
    // Add optimistic message
    // Call API
    // Handle response
  };

  return { messages, sendMessage, isLoading };
}

API Layer

The API layer consists of:

  • Axios instance with base URL configuration
  • Auth interceptor for automatic token injection
  • Error handling utilities
// src/lib/api.ts
const api = axios.create({
  baseURL: import.meta.env.VITE_API_URL,
  headers: { "Content-Type": "application/json" },
});

api.interceptors.request.use((config) => {
  const token = localStorage.getItem("token");
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

Context Layer

React Context provides global state:

  • AuthContext - User authentication state
  • No additional global stores needed (TanStack Query handles server state)
// src/context/AuthContext.tsx
interface AuthContextType {
  user: User | null;
  token: string | null;
  login: (token: string, user: User) => void;
  logout: () => void;
  isAuthenticated: boolean;
}

Component Categories

UI Components (components/ui/)

Base components from shadcn/ui following Radix UI primitives:

Component Purpose
Button Interactive button with variants
Card Content container with header/footer
Dialog Modal dialogs
Form Form handling with react-hook-form
Input Text input fields
Select Dropdown selection
Table Data tables
Tabs Tabbed navigation

Domain Components

Components organized by feature domain:

graph TB
    subgraph "Chat Domain"
        ML[MessageList]
        MB[MessageBubble]
        CI[ChatInput]
        SS[SessionSelector]
    end
    
    subgraph "Dashboard Domain"
        DM[DocumentManager]
        SL[StudentList]
        SC[SubjectCard]
        SO[StatsOverview]
    end
    
    subgraph "Layout Domain"
        AL[AppLayout]
        RA[RequireAuth]
        RS[RequireStudent]
        RP[RequireProfessor]
    end

Layout Components

Control application structure and access:

Component Purpose
AppLayout Main application shell with sidebar
RequireAuth Redirects unauthenticated users
RequireStudent Restricts to student role
RequireProfessor Restricts to professor/admin roles
RequireAdmin Restricts to admin role
PublicRoute Prevents authenticated users from accessing login
DefaultRedirect Redirects to role-appropriate page

State Management Strategy

graph TB
    subgraph "Global State"
        Auth[AuthContext<br/>User, Token]
    end
    
    subgraph "Server State"
        TQ[TanStack Query<br/>Sessions, Messages, Users]
    end
    
    subgraph "Local State"
        LS[useState/useReducer<br/>Form data, UI state]
    end
    
    Auth -->|provides| Components
    TQ -->|caches| Components
    LS -->|updates| Components
State Type Tool Examples
Authentication React Context User, token, login/logout
Server Data TanStack Query Sessions, messages, documents
UI State useState Modal open, selected tab
Form State React Hook Form Input values, validation

Error Handling

API Errors

// src/lib/errors.ts
export function getErrorMessage(error: unknown, fallback: string): string {
  if (isApiError(error)) {
    return error.response?.data?.detail || 
           error.response?.data?.message || 
           fallback;
  }
  if (error instanceof Error) {
    return error.message;
  }
  return fallback;
}

Error Display

Errors are displayed using toast notifications:

import { toast } from "sonner";

try {
  await api.post("/chat", data);
  toast.success("Mensaje enviado");
} catch (error) {
  toast.error(getErrorMessage(error, "Error al enviar mensaje"));
}

Performance Patterns

Query Caching

TanStack Query provides automatic caching:

export function useSessions() {
  return useQuery({
    queryKey: ["sessions"],
    queryFn: fetchSessions,
    staleTime: 30_000, // 30 seconds
  });
}

Optimistic Updates

Chat uses optimistic updates for instant feedback:

const sendMessage = async (content: string) => {
  // Immediately add user message to UI
  setMessages(prev => [...prev, userMessage]);
  
  try {
    const response = await api.post("/chat", { message: content });
    // Add real assistant response
    setMessages(prev => [...prev, assistantMessage]);
  } catch {
    // Remove optimistic message on error
    setMessages(prev => prev.filter(m => m.id !== userMessage.id));
  }
};

Code Splitting

Routes are lazily loaded for better initial load:

const ChatPage = lazy(() => import("./pages/chat/ChatPage"));
const DashboardPage = lazy(() => import("./pages/dashboard/DashboardPage"));

Security Considerations

Token Storage

JWT tokens are stored in localStorage:

  • ✅ Persists across sessions
  • ⚠️ Vulnerable to XSS (mitigated by CSP in production)

Auth Interceptor

All API requests automatically include the auth token:

api.interceptors.request.use((config) => {
  const token = localStorage.getItem("token");
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

Route Protection

Multiple layers of route guards ensure proper access:

<Route element={<RequireAuth />}>
  <Route element={<RequireStudent />}>
    <Route path="/chat" element={<ChatPage />} />
  </Route>
</Route>