Instruction file imported from cognizhi/sydekx-oss (
.github/instructions/frontend.instructions.md). Copyright stays with the author.
Frontend Conventions — SydeKx
Tech Stack
- React 19 + TypeScript 6.0 (strict mode)
- Vite 8 — dev server + bundler;
@alias maps tosrc/ - React Router v7 —
BrowserRouterwith protected/public route split - TanStack Query v5 — server state (documents, profiles, etc.)
- React Context — app state (auth, sessions, theme, settings)
- Tailwind CSS v4 + shadcn/ui (
new-yorkstyle,neutralbase) - Axios — HTTP client with JWT interceptors; native Fetch for SSE streams
- Supabase JS — realtime subscriptions only (not for auth)
- Lucide React — icons; no other icon libraries
File Organization
src/
├── components/
│ ├── ui/ # shadcn/ui primitives — never edit directly
│ ├── auth/ # LoginForm, RegisterForm, ProtectedRoute, etc.
│ ├── chat/ # ChatView, MessageBubble, MessageInput, profile selector
│ ├── code/ # Code tab (groups, repos, processing status)
│ ├── documents/ # DocumentsView, UploadZone, DocumentList, status badges
│ ├── graph/ # Graph visualization components
│ ├── knowledge/ # Knowledge base / IKL components
│ ├── layout/ # AppLayout, collapsible Sidebar, Header
│ ├── admin/ # AdminPanel, user management, agent profiles UI
│ ├── settings/ # UserSettingsView (secrets, tokens)
│ ├── about/ # About page
│ └── manual/ # User manual pages
├── contexts/ # AuthContext, SessionContext, ThemeContext, SiteSettingsContext
├── services/ # api.ts (axios instance), *Service.ts files
├── types/ # TypeScript interfaces, never import from elsewhere
├── lib/
│ ├── utils.ts # cn() helper
│ ├── env.ts # getEnv() for Docker + Vite env vars
│ └── supabase.ts # Supabase client (realtime only)
└── hooks/ # Custom React hooks
Component Conventions
-
Functional components only — no class components
-
One component per file; filename matches export name (PascalCase)
-
Co-locate component-specific types in the same file unless shared
-
Use
cn()from@/lib/utilsfor all className merging — never concatenate strings directly:import { cn } from "@/lib/utils" <div className={cn("base-class", isActive && "active-class", className)} /> -
New shadcn/ui components: add via
npx shadcn@latest add <component>— never write them from scratch
Styling
-
Tailwind CSS utility classes only — no inline styles, no CSS modules
-
Use semantic CSS variables (
bg-background,text-foreground,border,text-muted-foreground) — not hardcoded colors -
Dark mode via
.darkclass on<html>(managed byThemeContext) — usedark:variant -
Variants with CVA for reusable components:
const variants = cva("base-classes", { variants: { variant: { default: "...", destructive: "..." } }, defaultVariants: { variant: "default" }, })
State Management
Server state → TanStack Query:
const { data, isLoading, error } = useQuery({
queryKey: ["documents", filters],
queryFn: () => documentService.listDocuments(filters),
enabled: !!user,
staleTime: 30_000,
})
// Mutations with cache invalidation
const mutation = useMutation({
mutationFn: documentService.deleteDocument,
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["documents"] }),
})
App state → Context hooks: always consume via provided hooks, not useContext directly:
const { user, login, logout } = useAuth()
const { activeSession, createSession, messages } = useSession()
const { theme, setTheme } = useTheme()
Optimistic UI: use addLocalMessage() from SessionContext for chat messages before the server response arrives.
API / Services Layer
All HTTP calls go through the centralized Axios instance in @/services/api.ts which:
- Attaches
Authorization: Bearer <token>fromlocalStorageon every request - On 401 responses: clears auth state and redirects to
/login
Service files are plain objects of typed async functions — no classes:
// services/featureService.ts
export const featureService = {
async list(params?: ListParams): Promise<Feature[]> {
const { data } = await api.get("/api/feature", { params })
return data
},
async create(payload: CreateFeaturePayload): Promise<Feature> {
const { data } = await api.post("/api/feature", payload)
return data
},
}
Never call localStorage for tokens inside service files — the interceptor handles it.
SSE Streaming
Use the native fetch API (not Axios) for SSE — Axios does not support streaming:
const response = await fetch(`${API_URL}/api/chat/sessions/${sessionId}/stream?message=...`, {
headers: { Authorization: `Bearer ${token}` },
signal, // AbortSignal for cancellation
})
const reader = response.body!.getReader()
const decoder = new TextDecoder()
// parse `data: {...}\n\n` lines and dispatch by event `type`
SSE event types from the backend: thinking_complete, sub_agent_start, tool_call, stream, complete, error.
TypeScript Conventions
- All types/interfaces in
src/types/for shared types; co-locate in component file if component-only - Prefer
interfaceovertypefor object shapes - Use
Record<string, unknown>for dynamic/untyped payloads — avoidany - Type API responses explicitly — don't rely on inference from
axios.get()
Environment Variables
Always use getEnv() from @/lib/env.ts — it handles both Vite dev (import.meta.env) and Docker runtime (window.__ENV__):
import { getEnv } from "@/lib/env"
const API_URL = getEnv("VITE_API_URL")
Never use import.meta.env directly in components or services.
Routing & Auth Guards
ProtectedRoutewraps all authenticated routes — redirects to/loginif unauthenticated- Admin-only routes: check
user.role === "admin"inside the component, redirect otherwise - New routes: add to the
Routesblock inApp.tsxinside or outsideAppLayoutas appropriate
Realtime (Supabase)
Use the Supabase client from @/lib/supabase.ts for realtime subscriptions only (e.g., document ingestion status):
const channel = supabase
.channel(`document:${documentId}`)
.on("postgres_changes", { event: "UPDATE", schema: "public", table: "documents" },
(payload) => queryClient.setQueryData(["documents"], updater))
.subscribe()
// Always unsubscribe on cleanup
return () => { supabase.removeChannel(channel) }
TDD Requirement (non-negotiable)
This project follows Test Driven Development. A task is not complete until tests are written, run, and pass.
Framework: Vitest + @testing-library/react. Run from frontend/:
npm test # single run (CI mode)
npm run test:watch # watch mode during development
Test file placement: co-locate with the source file:
src/lib/utils.ts → src/lib/utils.test.ts
src/services/documentService.ts → src/services/documentService.test.ts
src/components/chat/ChatView.tsx → src/components/chat/ChatView.test.tsx
Coverage priorities (in order): happy path → edge cases → error paths → auth boundaries.
A task is only done when npm test exits with 0 failures and line coverage stays ≥ 80%.
Universal Rules
- Secrets: never commit secrets, credentials, or API keys.
- Linting: run
npm run lint && npx prettier --write .insidefrontend/before committing. - Type checking: run
npx tsc --noEmitand resolve all errors before committing. - TDD: write tests alongside implementation. 80% line coverage minimum.
- Branching: branch from
main(feat/<scope>,fix/<scope>,chore/<scope>, etc.). Never push directly tomain. - Commits: follow Conventional Commits —
feat(chat): add profile selector,fix(auth): redirect loop on token expiry. - PRs: open a PR for all changes.
mainrequires a code-owner approval + passing CI. - Files: do not delete or rename files without explicit instruction from tech-lead.