A web app to practice interviews (behavioral, coding/LeetCode, and project deep-dives) with streaming AI responses. It supports PDF resume ingestion, strict interview-type enforcement, and cost controls via rate limiting, caching, and token management.
- Multiple interview modes: behavioral, leetcode, and project
- PDF resume ingestion (client-side text extraction via pdfjs-dist)
- Streaming chat with OpenAI for real-time responses
- Enforced interview type with a server-side system directive for consistency
- Cost controls: GPT‑3.5 default, lower temperature, max token caps, history windowing, and resume trimming
- In-memory rate limiter and response cache to reduce usage/costs
- Local chat history saved to the browser (localStorage)
- Clean chat UI with type switching and history viewer
- Framework: Next.js 13 (App Router), React 18, TypeScript
- Styling: Tailwind CSS + custom CSS
- AI: OpenAI Chat Completions (via
openai+aistreaming helpers) - PDFs:
pdfjs-distfor client-side text extraction - Deployment: Vercel (Edge runtime for API streaming)
Prerequisites:
- Node.js 18+ and npm
- An OpenAI API key
Setup:
- Clone the repository
- Install dependencies
- Create a
.envfile with your OpenAI key - Run the dev server
Commands:
npm install
echo "OPENAI_API_KEY=sk-..." > .env
npm run devAvailable scripts:
npm run dev– start the dev servernpm run build– build for productionnpm run start– run the production buildnpm run lint– run ESLint
Environment variables:
OPENAI_API_KEY(required)
- You upload a resume (PDF). Text is extracted client-side and kept in memory/localStorage on the client.
- You choose an interview type. The app crafts a system prompt according to that mode and includes trimmed resume context.
- Messages are streamed from the Edge API route using OpenAI Chat Completions.
- The server enforces the selected interview type and applies cost controls (model, temperature, max tokens).
- A simple in-memory rate limiter and response cache reduce repeated usage and costs.
flowchart TD
%% Client
subgraph Client["Next.js Client (App Router)"]
Page["app/page.tsx"]
ChatPage["app/chat/page.tsx"]
RequestForm["RequestForm.tsx<br/>(PDF → text via pdfjs-dist)"]
Chat["Chat.tsx<br/>(UI, history windowing, resume trim)"]
Markdown["MarkdownRenderer.tsx"]
ChatHistory["ChatHistory.tsx"]
LocalStorage["localStorage<br/>resume, interviewType, sessions"]
end
%% Server
subgraph Server["Edge API Routes"]
Route["app/api/openai-gpt/route.ts<br/>(OpenAIStream, system guard)"]
RateLimiter["utils/rateLimiter.ts<br/>(in-memory)"]
Cache["utils/responseCache.ts<br/>(in-memory, TTL)"]
end
%% External
subgraph OpenAI["OpenAI API"]
Completions["Chat Completions"]
end
subgraph Assets["Static Assets"]
Public["public/*<br/>(avatars)"]
end
subgraph Deploy["Vercel"]
Edge["Vercel Edge Runtime"]
Env["OPENAI_API_KEY"]
end
%% Flows
RequestForm -->|Extract text| LocalStorage
Page --> LocalStorage
Page --> Chat
ChatPage --> Chat
Chat -->|streaming fetch| Route
Route --> RateLimiter
Route --> Cache
Cache -->|hit → return cached| Route
Route -->|OpenAIStream| Completions
Completions -->|tokens stream| Route
Route -->|stream + rate-limit headers| Chat
Chat --> ChatHistory
Page --> Public
Edge --- Route
Env --- Route
sequenceDiagram
participant U as User (Browser)
participant C as Chat.tsx (Client)
participant R as API Route (route.ts)
participant L as RateLimiter
participant X as Cache
participant O as OpenAI
U->>C: Send message
C->>R: POST /api/openai-gpt (messages, interviewType)
R->>L: check()
L-->>R: allow/deny
R->>X: get(lastUserMessageKey)
alt cache hit
X-->>R: cached response
R-->>C: stream cached response + headers
else cache miss
R->>O: stream completion (system + user history)
O-->>R: token stream
R->>X: put(key, response, TTL)
R-->>C: stream response + rate-limit headers
end
C-->>U: Render streaming tokens
app/
api/openai-gpt/route.ts # Edge API: streaming to OpenAI, cost guard, rate limit, cache
components/
Chat.tsx # Chat UI, history windowing, rate-limit UI
ChatHistory.tsx # Local session viewer
MarkdownRenderer.tsx # Markdown + code highlighting
RequestForm.tsx # PDF → text, interview type selector
Wrapper.tsx # Home page wrapper
chat/page.tsx # Chat page
globals.css # Global styles + imports
chat-page.css # Chat layout styles
clean-chat.css # Chat visual polish
chat-history.css # History drawer styles
layout.tsx # Root layout & metadata
page.tsx # Home page
utils/
fetchOpenAIResponse.ts # Streaming fetch helper
rateLimiter.ts # In-memory rate limiter
responseCache.ts # In-memory response cache (TTL)
public/
bob.jpg, user-avatar.jpg # Avatars
- Model: defaults to
gpt-3.5-turbo-1106(seeapp/api/openai-gpt/route.ts) - Temperature:
0.5to reduce rambling - Max tokens: tighter caps (e.g., ~400 for LeetCode, ~180 for others)
- History windowing: keeps first system + last ~8 messages
- Resume trimming: limits resume context to ~2000 chars
- Rate limiting: 20 requests/hour per (cookie) session by default
- Response cache: 24h TTL, up to 100 recent keys (last user message)
- Change model/temperature/tokens in
app/api/openai-gpt/route.ts - Adjust rate limits in
app/utils/rateLimiter.ts - Tune cache TTL/size in
app/utils/responseCache.ts - Update interview prompts in
app/components/Chat.tsx(getSystemPrompt)
- Push to GitHub (ensure
.envis not committed) - Import the repo in Vercel
- Set Environment Variable
OPENAI_API_KEY - Deploy (build uses Next.js App Router defaults)
Notes:
- The API route runs on the Edge runtime for low-latency streaming
- Consider moving rate limit/cache to Redis for production
- Resume text is extracted client-side. The trimmed resume content and your chat messages are sent to the API route to generate responses.
- No server-side database is used here; rate limiting and caching are in-memory.
- PDF text didn’t extract: The PDF may be scanned (image-based). Try a text-based PDF.
- Streaming doesn’t start: Check your
OPENAI_API_KEYand that your account has access/credits. - Rate limit reached: Wait until the reset time shown in the UI or reduce frequency.
- Build warnings: You can safely ignore minor Next.js metadata warnings; they don’t affect functionality.
Contributions are welcome!
- Fork the repository and create a feature branch
- Make changes and keep them focused (small PRs are easier to review)
- Run lint/build locally to validate
- Open a Pull Request with a clear description and screenshots when UI changes
Guidelines:
- Use clear commit messages (e.g., feat:, fix:, docs:, refactor:)
- Don’t commit secrets or
.env - Keep formatting consistent; run
npm run lint - Prefer small, incremental changes over large refactors
- Move rate limiter and cache from memory to Redis for production
- Add export/share for transcripts (Markdown/PDF)
- Model selector (with quotas) and temperature controls in UI
- i18n support and accessibility improvements (ARIA, keyboard nav)
- UI performance: dynamic import for syntax highlighter
- Session persistence across devices (optional server store)
- Conversation starters and structured evaluation rubrics
- Add tests (unit/integration) and CI for lint/build
- Optional: theme support and CSS consolidation
This project is licensed under the MIT License. See LICENSE for details.
- Next.js, React, Tailwind CSS
- OpenAI API and the
aistreaming helpers pdfjs-distfor client-side PDF text extractionreact-markdownandreact-syntax-highlighter
Happy coding — 👨💻