Skip to content

Frontend Architecture

Understanding the structure, patterns, and key technical decisions behind the Next.js frontend.

System Overview

The ai-tutor-ui is a Next.js 16 application that serves as the student-facing and admin-facing interface for the AI tutoring platform. It communicates exclusively with the FastAPI backend over HTTP.

Key Characteristics

  • App Router: Uses Next.js App Router (not the legacy Pages Router)
  • TypeScript strict mode: All code is fully typed; strict: true in tsconfig
  • Component-driven: UI built from shadcn/ui primitives composed into feature components
  • Course-level i18n: Localization scoped to course content, not the app shell
  • Token-based auth: JWT access + refresh tokens managed client-side
  • Markdown + math: Lesson content renders GitHub-flavored Markdown with LaTeX math via KaTeX

Technology Stack

Concern Choice
Framework Next.js 16 (App Router)
Language TypeScript (strict mode)
Styling Tailwind CSS + CSS custom properties (HSL theming)
Component library shadcn/ui (Radix UI primitives + Tailwind)
Forms React Hook Form + Zod
i18n Custom useCourseTranslations hook (next-intl installed but not used for app shell)
Testing Vitest + React Testing Library + MSW
Markdown react-markdown + remark-gfm + remark-math + rehype-katex
Icons Lucide React

Architecture Diagram

graph TB
    Browser[Browser]
    NextJS[Next.js App Router]
    Pages[Pages & Layouts]
    Components[React Components]
    Hooks[Custom Hooks]
    APILayer[API Layer - lib/api/]
    AuthClient[Auth Client - apiFetch]
    Contexts[React Contexts]
    Backend[FastAPI Backend :9898]

    Browser --> NextJS
    NextJS --> Pages
    Pages --> Components
    Components --> Hooks
    Components --> Contexts
    Components --> APILayer
    APILayer --> AuthClient
    AuthClient --> Backend

Project Structure

ai-tutor-ui/
├── app/                    # Next.js App Router pages
│   ├── layout.tsx          # Root layout (fonts, providers, toaster)
│   ├── page.tsx            # Home/landing page
│   ├── signin/             # Sign in page
│   ├── signup/             # Registration page
│   ├── signup-onboarding/  # Post-registration onboarding
│   ├── dashboard/          # Student dashboard
│   ├── courses/[id]/       # Course detail + modules
│   ├── notebook/           # Student notebook
│   ├── admin/              # Admin panel (courses, users, prompts, analytics, etc.)
│   ├── share/              # Public shared chat sessions
│   ├── chat-debug/         # Chat debugging tools
│   └── globals.css         # CSS custom properties (theme tokens)
├── components/
│   ├── ui/                 # shadcn/ui base components (button, card, dialog...)
│   ├── auth/               # Authentication components (RoleGuard)
│   ├── admin/              # Admin panel components
│   ├── dashboard/          # Dashboard components (CourseCarousel)
│   ├── features/           # Feature-specific components (CourseDialog, ModuleTree...)
│   ├── home/               # Landing page components
│   ├── notebook/           # Notebook components
│   ├── onboarding/         # Onboarding flow components
│   └── *.tsx               # Shared components (Header, MessageContent, LessonView...)
├── hooks/                  # Custom React hooks
│   ├── use-debounce.ts
│   └── use-toast.ts
├── lib/
│   ├── api/                # API layer — one file per resource (courses.ts, users.ts, modules.ts...)
│   ├── authClient.ts       # Core API client: apiFetch(), token management
│   ├── api-client.ts       # Server-side API helpers (public course listing)
│   ├── contexts/           # React contexts (RoleProvider, LanguageDirectionContext)
│   ├── i18n/               # Internationalization (useCourseTranslations, config, CourseI18nProvider)
│   ├── validations/        # Zod schemas for form validation (auth, course, module, content, onboarding)
│   ├── utils.ts            # Shared utilities (cn() for class merging)
│   └── *.ts                # Feature utilities (lesson-parser, parse-chat-response, theme-config, courseStyles)
├── types/                  # TypeScript type definitions (auth, course, role, notebook, interactive-ui, llm-provider)
├── messages/               # i18n translation files (en.json, ar.json)
├── tests/                  # Test infrastructure (setup.ts, test-utils.tsx, mocks/)
└── public/                 # Static assets

API Layer

All backend communication flows through a single client in lib/authClient.ts.

Core Client: apiFetch

apiFetch<T>(path, options) is the single entry point for every API call. It handles:

  • Attaching the JWT access token as a Bearer header
  • Automatic token refresh when a 401 is returned
  • JSON serialization of request bodies
  • Structured error handling via the ApiError class
// ApiError carries the HTTP status and parsed response body
class ApiError extends Error {
  status: number;
  data: unknown;
}

Tokens are stored in localStorage under two keys:

  • ai_tutor_access_token — short-lived JWT for API requests
  • ai_tutor_refresh_token — long-lived token used to obtain a new access token

Resource Modules

lib/api/ contains one file per backend resource. Each file exports plain async functions that call apiFetch:

lib/api/
├── courses.ts      # listCourses(), getCourse(id), createCourse(data)...
├── users.ts        # getMe(), updateUser(id, data)...
├── modules.ts      # listModules(courseId), createModule(data)...
├── content.ts      # getContentItem(id), createContent(data)...
└── prompts.ts      # listPrompts(), createPrompt(data)...

Components import directly from these modules. There's no global state layer for server data — components fetch what they need and manage loading/error state locally or via React context.

Authentication & Authorization

Token Flow

  1. User signs in or registers
  2. Backend returns an access token and a refresh token
  3. Both tokens are written to localStorage
  4. Every apiFetch call reads the access token and attaches it as Authorization: Bearer <token>
  5. On a 401 response, apiFetch calls the refresh endpoint, stores the new tokens, and retries the original request

Role System

RoleProvider (in lib/contexts/) wraps the entire app. On mount it fetches the current user's roles from the backend and makes them available via the useRoles() hook.

const { isInstructor, isPlatformAdmin, canManagePrompts, canCreateContent } = useRoles();

The role hierarchy from lowest to highest privilege:

student < ta < instructor < department_admin < university_admin < platform_admin

RoleGuard is a component wrapper for conditional rendering based on role checks:

<RoleGuard requireInstructor>
  <AdminControls />
</RoleGuard>

{/* Or with an explicit role list */}
<RoleGuard requiredRoles={["instructor", "department_admin"]}>
  <AdminControls />
</RoleGuard>

RoleProvider also listens for storage events so that role state stays consistent across browser tabs.

Theming & Styling

CSS Custom Properties

app/globals.css defines all color tokens as HSL values:

:root {
  --primary: 38 92% 50%;
  --secondary: 210 40% 96.1%;
  --muted: 210 40% 96.1%;
  --accent: 38 100% 86%;
  --destructive: 0 84.2% 60.2%;
}

.dark {
  --primary: 217.2 91.2% 59.8%;
  /* ... */
}

Dark mode is toggled by adding the .dark class to the <html> element.

Tailwind + shadcn/ui

Tailwind is configured with semantic color names that map to the CSS custom properties:

// tailwind.config.js
colors: {
  primary: "hsl(var(--primary))",
  secondary: "hsl(var(--secondary))",
  // ...
}

shadcn/ui components live in components/ui/ as copied source files (not an npm package). They use class-variance-authority (cva) for variant-based styling, clsx for conditional classes, and tailwind-merge to resolve conflicts. The cn() utility in lib/utils.ts combines all three.

Fonts

  • Inter — body text
  • Patrick Hand — decorative/display use
  • IBM Plex Sans Arabic — RTL content

Custom Animations

globals.css defines custom keyframes used across the app: accordion-down, accordion-up, fade-in, and loader-reveal.

Internationalization (i18n)

Course-Level Localization

The app shell (navigation, buttons, labels) is in English. Localization applies at the course level: each course has a language_code, and content within that course is rendered in that language.

This means i18n is not configured globally in Next.js. Instead, components that render course content call useCourseTranslations(languageCode).

useCourseTranslations

const { t, isRTL, direction, formatNumber } = useCourseTranslations(languageCode);

t("lesson", "progress")        // looks up messages/en.json or messages/ar.json
isRTL                          // true for Arabic, Hebrew, Farsi, Urdu
direction                      // "rtl" | "ltr"
formatNumber(42)               // "٤٢" for ar locale, "42" otherwise

Translation files live in messages/en.json and messages/ar.json.

RTL Support

RTL layout is applied conditionally based on isRTL. The patterns to follow:

  • flex-row-reverse with isRTL to swap flex item order
  • Logical CSS properties (ms-*, me-*, ps-*, pe-*) instead of physical (ml-*, mr-*)
  • rtl-flip class on directional icons (chevrons, arrows)

See ai-tutor-ui/docs/i18n-rtl-guide.md for the full RTL pattern reference.

Design Patterns

Form Validation

Forms use React Hook Form with Zod schemas defined in lib/validations/. The schema is the single source of truth for both runtime validation and TypeScript types:

// lib/validations/auth.ts
const signInSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
});

type SignInData = z.infer<typeof signInSchema>;

Component Composition

Feature components are built by composing shadcn/ui primitives from components/ui/. The pattern is:

  1. components/ui/ — unstyled or minimally styled primitives (Button, Card, Dialog...)
  2. components/features/ — domain components that compose primitives (CourseDialog, ModuleTree...)
  3. app/ pages — assemble feature components into full pages

Markdown Rendering

Lesson content and chat messages are rendered with react-markdown. The pipeline:

  • remark-gfm — GitHub-flavored Markdown (tables, strikethrough, task lists)
  • remark-math — parses $...$ and $$...$$ math delimiters
  • rehype-katex — renders parsed math as KaTeX HTML

Key Design Decisions

localStorage vs httpOnly Cookies for Tokens

Chose: localStorage

Pros: - Simple to implement in a client-rendered SPA - No server-side cookie handling needed - Works across subdomains without configuration

Cons: - Accessible to JavaScript (XSS risk) - Requires manual token attachment on every request

Mitigation: Short-lived access tokens (30 min), automatic refresh

Course-Level vs App-Level i18n

Chose: Course-level localization

Reasoning: The app UI is English-only. Only course content (lesson text, quiz labels, chat responses) needs localization. Applying next-intl globally would add complexity without benefit for the app shell. The useCourseTranslations hook keeps localization scoped to where it's actually needed.

shadcn/ui vs a Full Component Library

Chose: shadcn/ui

Pros: - Components are copied into the project — full ownership, no version lock-in - Built on Radix UI primitives (accessible by default) - Tailwind-native styling fits the existing setup - Easy to customize without fighting library internals

Cons: - Updates require manual re-copying of components - More files in the repo

App Router vs Pages Router

Chose: App Router

Reasoning: Next.js recommends App Router for new projects. It enables React Server Components, nested layouts, and colocated loading/error states. The project uses it primarily for its layout and routing capabilities rather than server-side data fetching.

Next Steps