open-notebook/frontend/src/CLAUDE.md

9.8 KiB

Frontend Architecture

Next.js React application providing UI for Open Notebook research assistant. Three-layer architecture: pages (Next.js App Router), components (feature-specific UI), and lib (data fetching, state management, utilities).

High-Level Data Flow

Pages (Next.js) → Components (feature-specific) → Hooks (queries/mutations)
                                                       ↓
                          Stores (auth/modal state) → API module → Backend

User interactions trigger mutations/queries via hooks, which communicate with the backend through the API module. Store state (auth, modals) flows back to components via hooks. Child CLAUDE.md files document specific modules in detail:

  • lib/api/CLAUDE.md: Axios client, FormData handling, interceptors
  • lib/hooks/CLAUDE.md: TanStack Query wrappers, SSE streaming, context building
  • lib/stores/CLAUDE.md: Zustand auth/modal state, localStorage persistence
  • lib/locales/CLAUDE.md: Internationalization (i18n) system, translation files
  • components/ui/CLAUDE.md: Radix UI primitives, CVA styling, accessibility

Architectural Layers

Pages (src/app/) — Next.js App Router

  • (auth)/login: Authentication entry point
  • (dashboard)/: Protected routes (notebooks, sources, search, models, etc.)
  • Directory-based routing; each page.tsx is a route endpoint
  • Key pattern: Pages call hooks to fetch data, render components with state
  • Router groups (auth), (dashboard) organize routes by feature without affecting URL

Components (src/components/) — Feature-Specific UI

  • layout: AppShell.tsx, AppSidebar.tsx — main layout wrapper used by all pages
  • providers: ThemeProvider, QueryProvider, ModalProvider — app-wide context setup
  • auth: LoginForm.tsx — authentication UI
  • common: CommandPalette, ErrorBoundary, ContextToggle, ModelSelector — shared across pages
  • ui: Reusable Radix UI building blocks (see child CLAUDE.md)
  • source, notebooks, search, podcasts: Feature-specific components consuming hooks

Component composition pattern: Pages → Feature components → UI components. Feature components handle page-level state (loading, error), UI components remain stateless and styled.

Lib (src/lib/) — Data & State Layer

lib/api/ — Backend Communication

  • client.ts: Central Axios instance with auth interceptor, FormData handling, 10-min timeout
  • query-client.ts: TanStack Query configuration
  • Resource modules (sources.ts, chat.ts, notebooks.ts, etc.): Endpoint-specific functions returning typed responses
  • Pattern: All requests go through apiClient; auth token auto-added from localStorage

lib/hooks/ — React Query + Custom Logic

  • Query hooks: useNotebookSources, useSources, useSource — TanStack Query wrappers with cache keys
  • Mutation hooks: useCreateSource, useUpdateSource, useDeleteSource — mutations with toast feedback + cache invalidation
  • Complex hooks: useNotebookChat, useSourceChat — session management, message streaming, context building
  • SSE streaming: useAsk — parses newline-delimited JSON from backend for multi-stage workflows
  • Pattern: Hooks return { data, isLoading, error, refetch } + action functions; cache invalidation on mutations

lib/stores/ — Application State

  • auth-store.ts: Authentication state (token, isAuthenticated) with 30-second check caching
  • Zustand + persist middleware: Auto-syncs sensitive state to localStorage
  • Pattern: Store actions (login(), logout(), checkAuth()) update state; consumed via hooks in components

lib/types/ — TypeScript Definitions

  • API request/response shapes, domain models (Notebook, Source, Note, etc.)
  • Ensures type safety across API calls and store mutations

lib/locales/ — Internationalization (i18n)

  • Locale files (en-US/, pt-BR/, zh-CN/, zh-TW/, ja-JP/): Translation strings organized by feature
  • i18n.ts: i18next configuration with language detection
  • use-translation.ts: Custom hook with Proxy-based t.section.key access pattern
  • Pattern: Components call useTranslation() hook; access strings via t.common.save, t.notebooks.title

Data & Control Flow Walkthrough

Example: Notebook Chat

  1. Page (notebooks/[id]/page.tsx) fetches initial data, passes notebookId to ChatColumn component
  2. Hook call (useNotebookChat()):
    • Queries sessions for notebook via TanStack Query
    • Sets up message state + context building logic
    • Returns { messages, sendMessage(), setModelOverride() }
  3. Component renders: ChatColumn displays messages, text input
  4. User sends message: Component calls sendMessage() hook
  5. Hook execution:
    • Builds context from selected sources/notes via buildContext() helper
    • Calls chatApi.sendMessage() (from API module)
    • Client-side optimistic update: adds message to local state before response
  6. Backend response arrives, TanStack Query updates cache
  7. Cache invalidation on other source/note mutations ensures stale UI refreshes

Example: File Upload with Source Creation

  1. Component (SourceDialog) renders form with file picker
  2. Hook (useFileUpload):
    • Converts file to FormData (JSON fields stringified)
    • Calls sourcesApi.create() with FormData
    • API client interceptor deletes Content-Type header (lets browser set multipart boundary)
  3. Toast notifications show progress
  4. Cache invalidation on success: queryClient.invalidateQueries(['sources'])
  5. Related queries auto-refetch: notebooks, sources list, etc.

Key Patterns & Cross-Layer Coordination

Caching & Invalidation

  • Query keys: QUERY_KEYS.notebook(id), QUERY_KEYS.sources(notebookId) — hierarchical structure
  • Broad invalidation: ['sources'] invalidates all source queries; trade-off between accuracy + performance
  • Auto-refetch: refetchOnWindowFocus: true on frequently-changing data (sources, notebooks)

Auth & Protected Routes

  • Proxy (src/proxy.ts): Redirects root / to /notebooks
  • Auth store: Validates token via /notebooks API call (actual validation, not JWT decode)
  • Interceptor: Adds Bearer {token} to all requests; 401 response clears auth and redirects to login

Modal State Management

  • Modal hooks: Components query modal state from stores
  • Context: Modals pass data (e.g., notebook ID) to child components
  • Pattern: One store per modal type; triggered by button clicks + data passing via hook arguments

Error Handling

  • API errors: All request failures propagate to consuming code; components show toast notifications
  • Toast feedback: Mutations show success/error toasts (from sonner library)
  • Error boundary: App-level error boundary catches React render errors; shows fallback UI

FormData Handling

  • JSON fields: Nested objects (arrays, objects) must be JSON stringified before FormData
  • Content-Type header: Removed by interceptor for FormData requests (lets browser set boundary)
  • Example: sources array converted to string via JSON.stringify() before appending to FormData

Component Organization Within Features

  • Feature folders (source/, notebooks/, podcasts/): Group related components
  • Composition: Larger components nest smaller ones; no deep prop drilling (state lifted to hooks)
  • Dialog patterns: Features define dialog components for inline actions (edit, create, delete)
  • Props: Components accept data + action callbacks from parent or hooks

Providers & Context Setup

Root layout (app/layout.tsx) wraps app with (outermost → innermost):

  1. ErrorBoundary — React error boundary (catches all render errors)
  2. ThemeProvider — next-themes for light/dark mode
  3. QueryProvider — TanStack Query client
  4. I18nProvider — i18next initialization and language loading overlay
  5. ConnectionGuard — checks backend connectivity on startup
  6. Toaster — sonner toast notification system (inside ConnectionGuard)

Important Gotchas & Design Decisions

  • Token storage: Stored in localStorage under auth-storage key (Zustand persist); consumed by API interceptor
  • Base URL discovery: API client fetches base URL from runtime config on first request (async; can be slow on startup)
  • Optimistic updates: Chat messages added to state before server confirmation; removed on error
  • Modal lifecycle: Dialogs not auto-reset; parent must clear form state after submit
  • Focus management: Dialog auto-focuses first input; can cause layout shifts if inputs are conditional
  • Cache invalidation breadth: Trade-off between precision + simplicity; broad invalidation simpler but may over-fetch

How to Add a New Feature

  1. Create page: app/(dashboard)/feature/page.tsx — calls hooks, renders components
  2. Create feature components: components/feature/ — compose UI + business logic
  3. Add hooks (if data needed): lib/hooks/useFeature.ts — TanStack Query wrapper
  4. Add API module (if backend call needed): lib/api/feature.ts — resource-specific functions
  5. Add types: lib/types/api.ts — request/response shapes
  6. Use UI components: Import from components/ui/ for consistent styling
  7. Handle auth: Middleware redirects unauthenticated users; no special handling needed in component

Testing

  • Hooks: Mock API functions, wrap in QueryClientProvider, assert query/mutation behavior
  • Components: Mock hooks via vi.fn(), test rendering + user interactions
  • API calls: Mock axios interceptors; test request/response shapes
  • Stores: Mock store state, test mutations via act(), assert state changes

See child CLAUDE.md files for module-specific testing patterns.