InterviewLab frontend is a modern Next.js 14 application built with TypeScript, React, and Tailwind CSS. It provides a responsive, accessible interface for conducting voice-based technical interviews.
graph TB
subgraph NextJS["Next.js App Router"]
A[Pages] --> B[Components]
B --> C[Hooks]
C --> D[API Client]
end
subgraph State["State Management"]
E[Zustand Store] --> F[React Query]
F --> D
end
subgraph External["External Services"]
D --> G[Backend API]
H[LiveKit Client] --> I[LiveKit Server]
end
B --> H
F --> G
The App Router provides server components and route-based code splitting. Zustand manages client-side auth state, while React Query handles server state with automatic caching and background refetching. The API client uses Axios interceptors to inject auth tokens and handle 401 redirects. LiveKit Client connects separately via WebSocket, with connection state managed by the useLiveKitRoom hook.
| Technology | Version | Purpose |
|---|---|---|
| Next.js | 14 (App Router) | React framework, routing, SSR |
| React | 19.2 | UI library |
| TypeScript | 5+ | Type safety |
| Tailwind CSS | 4 | Styling |
| shadcn/ui | Latest | Component library |
| Zustand | 5.0 | State management |
| TanStack Query | 5.90 | Data fetching, caching |
| LiveKit Client | 2.16 | Real-time voice communication |
| Monaco Editor | 4.7 | Code editor |
| Axios | 1.13 | HTTP client |
frontend/
├── app/ # Next.js App Router
│ ├── (auth)/ # Auth routes (login, register)
│ ├── dashboard/ # Protected dashboard routes
│ │ ├── interviews/ # Interview management
│ │ ├── resumes/ # Resume management
│ │ ├── analytics/ # Analytics dashboard
│ │ └── sandbox/ # Standalone sandbox
│ ├── layout.tsx # Root layout
│ └── providers.tsx # React Query provider
├── components/ # React components
│ ├── interview/ # Interview-specific components
│ ├── analytics/ # Analytics charts
│ ├── layout/ # Layout components
│ └── ui/ # shadcn/ui components
├── lib/ # Utilities
│ ├── api/ # API client & endpoints
│ └── store/ # Zustand stores
├── hooks/ # Custom React hooks
└── public/ # Static assets
| Component | Purpose | Key Features |
|---|---|---|
| voice-video.tsx | Main interview interface | LiveKit connection, audio visualization |
| sandbox.tsx | Code editor & execution | Monaco editor, code submission, results |
| transcription-display.tsx | Conversation transcript | Real-time transcription display |
| room-controls.tsx | Interview controls | Mute, disconnect, settings |
API Client (lib/api/client.ts):
- Axios-based HTTP client
- Automatic token injection
- Error handling & 401 redirect
- Request/response interceptors
API Endpoints (lib/api/):
auth.ts- Authenticationinterviews.ts- Interview CRUD, code submissionresumes.ts- Resume upload, analysisvoice.ts- LiveKit token generation
Example Usage:
import { interviewsApi } from "@/lib/api/interviews";
// Create interview
const interview = await interviewsApi.create({
title: "Python Engineer Interview",
resume_id: 123,
});
// Submit code
await interviewsApi.submitCode(interviewId, code, "python");sequenceDiagram
participant U as User
participant F as Frontend
participant A as API
participant LK as LiveKit
U->>F: Start Interview
F->>A: POST interviews id start
A->>F: token url room_name
F->>LK: Connect token url
LK->>F: Connected
F->>U: Audio stream active
The frontend requests a LiveKit token from the backend, which generates a JWT with room permissions. The token includes the room name (interview-{id}) and participant identity. Once connected, the agent bootstraps and the frontend subscribes to audio tracks. The useLiveKitRoom hook manages connection lifecycle, automatically handling reconnection on network failures and cleaning up resources on unmount.
Location: hooks/use-livekit-room.ts
Features:
- Automatic connection management
- Audio track subscription
- Connection state tracking
- Error handling & reconnection
Usage:
const { room, state, isConnected, connect, disconnect } = useLiveKitRoom({
token: livekitToken,
url: livekitUrl,
onConnected: (room) => console.log("Connected"),
onDisconnected: () => console.log("Disconnected"),
});Connection States:
idle- Not connectedconnecting- Connection in progressconnected- Active connectiondisconnected- Connection losterror- Connection error
Auth Store (lib/store/auth-store.ts):
interface AuthStore {
user: User | null;
token: string | null;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
isAuthenticated: boolean;
}Data Fetching:
- Automatic caching
- Background refetching
- Optimistic updates
- Error handling
Example:
const { data, isLoading, error } = useQuery({
queryKey: ["interview", id],
queryFn: () => interviewsApi.get(id),
refetchInterval: 2000, // Poll every 2s for active interviews
});| Feature | Implementation |
|---|---|
| Editor | Monaco Editor (VS Code editor) |
| Languages | Python, JavaScript |
| Execution | Real-time code execution via API |
| Results | stdout, stderr, exit code display |
| Submission | Submit code to interview for review |
Location: components/interview/sandbox.tsx
Key Functionality:
- Code Editing: Monaco editor with syntax highlighting
- Language Selection: Python/JavaScript toggle
- Local Execution: Test code before submission
- Code Submission: Submit to interview orchestrator
- Polling: Check for agent guidance/feedback
Code Submission Flow:
graph LR
A[User writes code] --> B[Click Submit]
B --> C[POST interviews id submit-code]
C --> D[Code saved to interview]
D --> E[Agent processes code]
E --> F[Code review response]
Code is saved to the database first, then the user's voice message triggers the orchestrator with current_code set. The orchestrator routes directly to code_review, bypassing intent detection. The sandbox service executes code in isolated Docker containers, and results are appended to code_submissions via reducer. The frontend polls the interview state to display code review feedback when available.
| Route | Purpose | Auth Required |
|---|---|---|
/ |
Landing page | No |
/login |
User login | No |
/register |
User registration | No |
/dashboard |
Main dashboard | Yes |
/dashboard/interviews |
Interview list | Yes |
/dashboard/interviews/[id] |
Interview page | Yes |
/dashboard/resumes |
Resume list | Yes |
/dashboard/analytics |
Analytics dashboard | Yes |
Implementation: components/auth/protected-route.tsx
<ProtectedRoute>
<DashboardLayout>{/* Protected content */}</DashboardLayout>
</ProtectedRoute>Colors:
- Primary: Professional blue (
hsl(221, 83%, 53%)) - Secondary: Subtle gray
- Destructive: Red for errors
- Muted: Light backgrounds
Components:
- shadcn/ui components (Radix UI primitives)
- Tailwind CSS utilities
- Dark mode support via
next-themes
| Breakpoint | Usage |
|---|---|
sm (640px) |
Mobile landscape |
md (768px) |
Tablet |
lg (1024px) |
Desktop |
xl (1280px) |
Large desktop |
- Dynamic Imports: Monaco editor loaded dynamically
- Route-based splitting: Next.js automatic code splitting
- Component lazy loading: Heavy components loaded on demand
- React Query: API response caching
- Next.js: Static asset caching
- Browser: Service worker (if implemented)
- Tree shaking: Unused code eliminated
- Minification: Production builds minified
- Compression: Gzip/Brotli enabled
cd frontend
npm install
cp .env.local.example .env.local
# Edit .env.local with API URL
npm run devNEXT_PUBLIC_API_URL=http://localhost:8000
NEXT_PUBLIC_LIVEKIT_URL=ws://localhost:7880| Script | Purpose |
|---|---|
npm run dev |
Start development server |
npm run build |
Build for production |
npm run start |
Start production server |
npm run lint |
Run ESLint |
# TypeScript check
npx tsc --noEmit
# Or via IDE (VS Code with TypeScript extension)// 1. Define API function
export const interviewsApi = {
get: async (id: number): Promise<Interview> => {
return apiClient.get(`/api/v1/interviews/${id}`);
},
};
// 2. Use in component with React Query
const { data, isLoading } = useQuery({
queryKey: ["interview", id],
queryFn: () => interviewsApi.get(id),
});// 1. Get token from API
const { data: tokenData } = useQuery({
queryKey: ["livekit-token", interviewId],
queryFn: () => voiceApi.getToken({ room_name: `interview-${interviewId}` }),
});
// 2. Connect with hook
const { room, isConnected } = useLiveKitRoom({
token: tokenData?.token,
url: tokenData?.url,
});Automatic Handling:
- 401 → Redirect to login
- Network errors → User-friendly messages
- Validation errors → Display in forms
Manual Handling:
try {
await interviewsApi.create(data);
} catch (error) {
if (error instanceof ApiError) {
toast.error(error.message);
}
}Connection Errors:
- Automatic reconnection attempts
- User notification on failure
- Graceful degradation
## Deployment
### Vercel (Recommended)
1. Connect GitHub repository
2. Set environment variables
3. Deploy automatically on push
**Build Settings:**
- Framework: Next.js
- Root Directory: `frontend`
- Build Command: `npm run build`
- Output Directory: `.next`
### Environment Variables (Production)
```bash
NEXT_PUBLIC_API_URL=
NEXT_PUBLIC_LIVEKIT_URL=wss://your-project.livekit.cloud
| Issue | Solution |
|---|---|
| Build fails | Check TypeScript errors, verify dependencies |
| API calls fail | Verify NEXT_PUBLIC_API_URL is set correctly |
| LiveKit won't connect | Check token generation, verify WebSocket URL |
| Code editor not loading | Ensure Monaco Editor is dynamically imported |
| Styling issues | Verify Tailwind config, check class names |
- Type Safety: Always use TypeScript types
- Error Boundaries: Wrap components in error boundaries
- Loading States: Show loading indicators for async operations
- Optimistic Updates: Update UI before API confirmation
- Accessibility: Use semantic HTML, ARIA labels
- Performance: Lazy load heavy components
- Security: Never expose API keys in client code