docs/7-DEVELOPMENT/frontend.md
How the Next.js app is layered and how data flows through it. Normative rules (commands, i18n, gotchas) live in frontend/AGENTS.md; this page is the mental model.
Pages (src/app/, App Router) → Feature components (src/components/) → Hooks (src/lib/hooks/)
↓
Stores (src/lib/stores/) → API modules (src/lib/api/) → Backend
(auth) / (dashboard) organize routes without affecting URLs. Pages call hooks and render components.source/, notebooks/, podcasts/, …) own page-level state (loading, error); components/ui/ are stateless Radix UI wrappers styled with Tailwind + CVA.src/lib/hooks/) — TanStack Query wrappers. Query hooks return { data, isLoading, error, refetch }; mutation hooks invalidate caches and toast. Complex hooks (useNotebookChat, useAsk) add session management, context building, SSE streaming.src/lib/stores/) — Zustand for auth and modal state; persist middleware syncs to localStorage (auth token under auth-storage).src/lib/api/) — namespaced typed clients (sourcesApi.list(), …) over a single axios instance with auth/FormData/401 interceptors.Provider tree in app/layout.tsx (outermost → innermost): ErrorBoundary → ThemeProvider → QueryProvider → I18nProvider → ConnectionGuard → Toaster.
notebooks/[id]/page.tsx passes notebookId to ChatColumn.useNotebookChat() queries sessions, manages message state, returns { messages, sendMessage(), setModelOverride() }.buildContext() assembles selected sources/notes (token/char counts), calls chatApi.sendMessage(), and applies an optimistic update (message added locally, removed on error).SourceDialog collects the file; useFileUpload builds FormData — nested JSON fields are stringified.queryClient.invalidateQueries(['sources']) refetches lists; useSourceStatus polls every 2s while the source is processing.Query keys are hierarchical (QUERY_KEYS.sources(notebookId)), but invalidation is deliberately broad (['sources'] catches everything) — a precision/simplicity trade-off. Frequently changing data uses refetchOnWindowFocus: true.
The token is validated by an actual API call (/notebooks), not JWT decoding, with a 30-second cache in the auth store. The response interceptor clears auth and redirects to /login on 401. Logout is client-side only.
getApiErrorMessage() (lib/utils/error-handler.ts) tries an i18n mapping first, then falls back to the backend's descriptive message — which the backend error-classification system already makes user-friendly (see architecture.md). Mutations surface errors as toasts; an app-level ErrorBoundary catches render errors.