An AI-powered technical interview preparation platform that generates role-specific questions, conducts voice-based interviews, and provides detailed feedback with scoring.
Getting Started β’ Features β’ Architecture β’ Azure Setup β’ Contributing
- Overview
- Key Features
- How It Works
- Technology Stack
- Architecture
- Getting Started
- Azure Services Setup
- Project Structure
- Seniority-Based Question Generation
- Evaluation Metrics
- API Reference
- Offline Response Sync
- Testing
- Cost Estimation
- Documentation
- Roadmap
- Contributing
- License
AI Tech Interview is a fullstack application designed to help software developers prepare for technical interviews through realistic, voice-based practice sessions. The platform leverages Azure AI services to:
- Generate tailored interview questions based on your target role and job description
- Read questions aloud using natural-sounding neural voices
- Record and transcribe your responses in real-time
- Evaluate answers and provide detailed feedback with actionable improvements
Traditional interview preparation often lacks the pressure and spontaneity of real interviews. This platform bridges that gap by:
- π€ Voice-first interaction - Practice speaking your answers, not just writing them
- π― Role-specific questions - Questions tailored to your exact target position and seniority level
- π Objective feedback - AI-powered evaluation with consistent scoring criteria
- β‘ Instant results - No waiting for feedback; get insights immediately after each session
| Feature | Description |
|---|---|
| π― Seniority-Aligned Questions | Questions are generated matching your target seniority level (Junior, Semi-Senior, Senior) |
| π€ Voice Interaction | Text-to-Speech reads questions; Speech-to-Text captures your responses |
| β±οΈ Timed Responses | Each question has a time limit (1-10 min) based on category with visible countdown |
| π€ AI-Powered Evaluation | GPT-4o-mini analyzes responses against expected competencies |
| π Detailed Scoring | Multi-dimensional scoring across 6 evaluation criteria |
| πΌ Role Customization | Supports any technical role with custom job descriptions |
| π Structured Feedback | Strengths, improvements, and actionable suggestions |
| π Session History | Track progress across multiple practice sessions |
(The voice-driven interview loop)
πΈ See more visuals: Check docs/README.md for architecture diagrams, screenshots, and detailed component descriptions.
stateDiagram-v2
[*] --> QuestionDisplayed
QuestionDisplayed --> Recording : User Clicks "Start"
state Recording {
[*] --> CountingDown
CountingDown --> Timeout : Timer reaches 0
CountingDown --> ManualStop : User clicks "Stop"
}
Timeout --> Processing : Auto-save
ManualStop --> Processing : Save
Processing --> [*] : Transcript Ready
(State diagram of the recording process)
Key Points:
- β User must press a button to start recording (gives time to think)
- β Visible countdown timer shows remaining time (1-10 min based on category)
- β Recording auto-stops when time runs out
- β User can manually stop earlier if finished
- β Whatever was spoken is saved and transcribed
| Layer | Technology | Version | Purpose |
|---|---|---|---|
| Frontend | Next.js (App Router) | 16.x | React framework with Server Components |
| Runtime | React | 19.x | UI library with concurrent features |
| Language | TypeScript | 5.x | Type-safe development |
| Styling | Tailwind CSS | 4.x | Utility-first CSS with @theme inline |
| AI/LLM | Azure OpenAI | GPT-4o-mini | Question generation & evaluation |
| Text-to-Speech | Azure Speech Service | Neural voices | Reading questions aloud |
| Speech-to-Text | Azure Speech Service | Real-time STT | Transcribing user responses |
| Database | PostgreSQL | - | Session and response storage |
- Turbopack by default - No need for
--turbopackflag - Server Actions - Seamless server-side operations without explicit API routes
- React 19 - Latest concurrent features and improved performance
- App Router - Modern file-based routing with layouts and nested routes
- Streaming - Real-time response streaming support
- TypeScript First - Excellent type safety and developer experience
(The hybrid architecture: A local Next.js app connecting to Azure AI services)
- Node.js 20.x or higher
- pnpm
- Azure subscription with:
- Azure OpenAI Service access
- Azure Speech Service resource
- Modern browser with microphone access (Chrome, Firefox, Edge recommended)
- Clone the repository
git clone https://github.com/cristofima/AI-Tech-Interview.git
cd AI-Tech-Interview- Install dependencies
pnpm install- Configure environment variables
Copy the example file and update with your Azure credentials:
cp .env.local.example .env.localThen edit .env.local with your values:
# Azure OpenAI Configuration
AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com/
AZURE_OPENAI_API_KEY=your-api-key
AZURE_OPENAI_DEPLOYMENT=gpt-5.4-mini
AZURE_OPENAI_MODEL=gpt-5.4-mini
AZURE_OPENAI_API_VERSION=v1
# Azure Speech Service Configuration
AZURE_SPEECH_KEY=your-speech-key
AZURE_SPEECH_REGION=eastusπ‘ Terraform Users: Run
terraform output -raw env_file_content > .env.localin theinfra/folder to auto-generate this file after provisioning.
Also set the PostgreSQL connection string and apply the schema:
# .env.local
DATABASE_URL=<your-postgresql-connection-string>
pnpm exec prisma generate
pnpm exec prisma db pushUse
db pushonly for a new development database after reviewing the target. For an existing database, review the additive response-sync SQL, then apply it withpnpm exec prisma db execute --file prisma\sql\001_offline_sync_columns.sql. Do not use--accept-data-lossfor this upgrade. Prisma CLI currently loads.env, not.env.local; provideDATABASE_URLthrough the shell environment or the existing.envconfiguration without committing credentials.
- Run the development server
pnpm devNote: Next.js 16 uses Turbopack by default - no
--turbopackflag needed!
- Open your browser
Navigate to http://localhost:3000
- Go to Azure Portal
- Search for "Azure OpenAI" and click Create
- Select your subscription and resource group
- Choose a region (e.g., East US)
- Provide a unique name for your resource
- Select pricing tier (Standard S0)
- Review and create
- Navigate to your Azure OpenAI resource
- Go to Model deployments β Manage Deployments
- Click Create new deployment
- Select model:
gpt-5.4-mini, version2026-03-17, deployment type Global Standard - Provide a deployment name (e.g.,
gpt-5.4-mini) - Set capacity within the available model quota for your active subscription
- Go to Keys and Endpoint in your resource
- Copy KEY 1 or KEY 2 β
AZURE_OPENAI_API_KEY - Copy Endpoint β
AZURE_OPENAI_ENDPOINT
- Go to Azure Portal
- Search for "Speech" and select Speech Services
- Click Create
- Select your subscription and resource group
- Choose a region (e.g., East US)
- Provide a unique name
- Select pricing tier (Free F0 for development, Standard S0 for production)
- Review and create
- Go to Keys and Endpoint in your Speech resource
- Copy KEY 1 β
AZURE_SPEECH_KEY - Note the Location/Region β
AZURE_SPEECH_REGION
| Model | Version | Features |
|---|---|---|
gpt-5.4-mini |
2026-03-17 | GA; Chat Completions, JSON Mode, Structured Outputs; default deployment |
gpt-5.4 |
2026-03-05 | GA; Chat Completions, JSON Mode, Structured Outputs; higher-capability alternative |
Verify the model lifecycle and regional availability before provisioning. Global Standard can process requests outside the resource region.
The client defaults to the v1 GA API. AZURE_OPENAI_DEPLOYMENT is the deployment name; AZURE_OPENAI_MODEL is the underlying model name and defaults to gpt-5.4-mini. GPT-5 requests use max_completion_tokens and low reasoning effort, without temperature; reasoning consumes part of the completion budget. For an existing GPT-4 deployment with a custom name, set AZURE_OPENAI_MODEL explicitly to preserve its sampling parameters. A dated AZURE_OPENAI_API_VERSION still selects the legacy Azure client.
AI-Tech-Interview/
βββ infra/ # Terraform Infrastructure-as-Code
β βββ main.tf # Azure resources (OpenAI, Speech)
β βββ variables.tf # Input variables
β βββ outputs.tf # Output values
β βββ versions.tf # Terraform & provider versions
β βββ README.md # Infrastructure documentation
βββ src/ # Next.js source code
β βββ app/ # App Router (pages, layouts, API)
β β βββ api/ # API Routes (sessions, responses, evaluate, speech)
β β βββ history/ # Session history page
β β βββ interview/ # Interview room page
β β βββ results/ # Results dashboard page
β β βββ globals.css # Global styles + Tailwind theme
β β βββ layout.tsx # Root layout
β β βββ page.tsx # Home page
β βββ components/ # React Components
β β βββ InterviewRoom.tsx # Main interview logic
β β βββ OfflineStatusIndicator.tsx # Sync status panel (pending / retrying / needs attention)
β β βββ ResponseSyncBadge.tsx # Per-answer sync status
β β βββ PendingUploadsNotice.tsx # Results-page warning for unsynced answers
β β βββ PermissionsCheck.tsx # Mic/Speech permissions
β β βββ ScoreCard.tsx # Result visualization
β β βββ SessionForm.tsx # Setup form
β βββ generated/ # Generated code (Prisma)
β βββ hooks/ # Custom Hooks
β β βββ useAudioRecorder.ts # Audio recording logic
β β βββ useOfflineSupport.ts # React binding for the sync controller
β β βββ useSpeechRecognition.ts # Azure STT
β β βββ useSpeechSynthesis.ts # Azure TTS
β βββ lib/ # Libraries & Utilities
β β βββ azure-openai.ts # OpenAI client
β β βββ azure-speech.ts # Speech SDK
β β βββ offline-storage.ts # IndexedDB store for recorded answers
β β βββ response-sync.ts # Upload engine (claim β upload β confirm)
β β βββ sync-controller.ts # Cross-tab coordination + scheduler
β β βββ response-submission.ts # Server-side idempotent persistence
β β βββ prisma.ts # DB client
β β βββ prompts.ts # System prompts
β β βββ utils.ts # Helpers
β βββ types/ # TypeScript Definitions
βββ prisma/ # Schema + prisma/sql (manual schema changes)
βββ tests/ # unit (mocked) and integration (PostgreSQL) tests
βββ public/ # Static assets
βββ scripts/ # Utility scripts (diagrams, etc.)
βββ docs/ # Documentation & Images
βββ diagrams/ # Architecture diagrams
βββ screenshots/ # Application screenshots
| Folder | Purpose |
|---|---|
infra/ |
Terraform IaC for Azure resources (kept at root for clear separation) |
src/ |
All Next.js application code (cleaner root directory) |
public/ |
Static assets (must remain at root for Next.js) |
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;@import "tailwindcss";
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules", "infra"]
}import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
];
export default eslintConfig;When creating an interview session, users provide the following fields:
| Field | Description | Example | Validation |
|---|---|---|---|
| Role Title | The target position with seniority level | "Senior FullStack .NET/Angular Developer" | β Required, min 10 characters |
| Company Name | The company you're interviewing for | "Google", "Microsoft", "Amazon" | βͺ Optional |
| Job Description | Full job posting or key responsibilities | "Design and implement scalable APIs..." | β Required, min 50 characters |
Note: Role Title and Job Description are mandatory. The Company Name is optional but can help tailor questions to the company's culture and tech stack.
Important: Both fields are mandatory. The system will not generate questions without a complete role title and job description. The seniority level (Junior, Mid, Senior) is extracted from the role title to ensure questions match the expected competency level.
Questions are generated to match the specified seniority level:
| Target Seniority | Senior Questions | Semi-Senior Questions | Junior Questions |
|---|---|---|---|
| Junior | 0% | 20% | 80% |
| Semi-Senior/Mid | 20% | 60% | 20% |
| Senior | 70-80% | 20-30% | 0% |
For a Senior FullStack .NET/Angular Developer role, the question distribution would be:
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 10 INTERVIEW QUESTIONS β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β ββββββββββββββββββββββββββββββββββββββββ 70-80% Senior β
β ββββββββββββ 20-30% Semi-Seniorβ
β 0% Junior β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Each question category has a recommended response time based on real interview scenarios:
| Category | Description | Examples | Time Limit | Rationale |
|---|---|---|---|---|
| Technical | Deep-dive into specific technologies | ".NET Core DI, Angular signals, RxJS" | β±οΈ 1-4 min | Direct answers with depth |
| System Design | Architecture and scalability | "Design a real-time notification system" | β±οΈ 5-10 min | Condensed version of real 30-45 min interviews |
| Behavioral | Leadership, communication, problem-solving | "Describe a technical decision you influenced" | β±οΈ 2-5 min | Full STAR method response |
| Problem Solving | Algorithmic thinking and debugging | "How would you optimize this query?" | β±οΈ 2-6 min | Explain approach + solution |
| Constraint | Value | Description |
|---|---|---|
| Minimum | 60 seconds (1 min) | Ensures meaningful, complete responses |
| Maximum | 600 seconds (10 min) | Allows thorough System Design answers |
| Default | 240 seconds (4 min) | Standard for most technical questions |
Why 10 minutes max? In real interviews, System Design questions take 30-45 minutes. Since this is a practice tool where users prepare answers beforehand, 10 minutes allows for a condensed but realistic response covering: requirements clarification, high-level design, key components, and trade-offs.
How it works: When the user presses the "Start Recording" button, a visible countdown timer begins on screen. The recording automatically stops when the time limit is reached, or the user can manually stop it earlier. The system saves whatever the user managed to say within the time limit.
The AI uses this structured prompt to generate seniority-aligned questions:
const systemPrompt = `You are a technical interview expert specializing in software engineering roles.
CRITICAL RULES:
1. Questions MUST align with the seniority level specified in the role title
2. For Senior roles: 70-80% Senior-level questions, 20-30% Semi-Senior, 0% Junior
3. For Mid-level roles: 20% Senior, 60% Mid-level, 20% Junior
4. For Junior roles: 0% Senior, 20% Mid-level, 80% Junior
5. Senior questions should test: architecture decisions, system design, leadership, mentoring
6. Never include junior-level questions for senior positions
Generate 10 interview questions based on the role and job description provided.
Output JSON format:
{
"questions": [
{
"id": 1,
"question": "...",
"difficulty": "senior|mid|junior",
"category": "technical|behavioral|system-design|problem-solving",
"expectedTopics": ["topic1", "topic2"],
"timeLimit": 240 // seconds (min: 60, max: 600)
}
]
}
Time limit guidelines by category:
- technical: 60-240 seconds (1-4 min)
- system-design: 300-600 seconds (5-10 min)
- behavioral: 120-300 seconds (2-5 min)
- problem-solving: 120-360 seconds (2-6 min)`;| Metric | Weight | Description |
|---|---|---|
| Relevance | 25% | How well the answer addresses the specific question asked |
| Technical Accuracy | 25% | Correctness of technical concepts, terminology, and best practices |
| Clarity | 20% | Clear, concise communication without rambling or tangents |
| Depth | 15% | Level of detail, thoroughness, and real-world examples |
| Structure | 10% | Logical organization (problem β approach β solution) |
| Confidence | 5% | Speech fluency, minimal filler words ("um", "uh", "like") |
| Score Range | Rating | Description | Hiring Signal |
|---|---|---|---|
| 90-100 | π Excellent | Exceptional responses demonstrating mastery | Strong hire |
| 75-89 | β Good | Solid answers with minor gaps | Hire with minor concerns |
| 60-74 | Adequate but needs improvement | Consider with reservations | |
| 40-59 | π Needs Work | Significant gaps in knowledge or communication | Not ready |
| 0-39 | β Poor | Major deficiencies requiring substantial preparation | Major concerns |
const evaluationPrompt = `You are an expert technical interviewer evaluating a candidate response.
CONTEXT:
- Role: {role}
- Seniority Expected: {seniority}
- Question: {question}
- Expected Topics: {expectedTopics}
- Candidate Response: {transcribedResponse}
EVALUATION CRITERIA:
Score each criterion from 0-100:
1. Relevance (25%): Does the answer directly address what was asked?
2. Technical Accuracy (25%): Are concepts, terminology, and best practices correct?
3. Clarity (20%): Is the answer clear, concise, and well-articulated?
4. Depth (15%): Does it show real-world experience and thorough understanding?
5. Structure (10%): Is there a logical flow (context β approach β outcome)?
6. Confidence (5%): Is speech fluent with minimal filler words?
SENIORITY EXPECTATIONS:
- For Senior roles: Expect architectural thinking, leadership examples, mentoring mentions
- For Mid-level roles: Expect solid technical execution and problem-solving
- For Junior roles: Expect foundational knowledge and learning attitude
Output JSON:
{
"scores": {
"relevance": 85,
"technicalAccuracy": 90,
"clarity": 75,
"depth": 80,
"structure": 70,
"confidence": 85
},
"overallScore": 82,
"performanceBand": "good",
"feedback": {
"strengths": ["Clear explanation of...", "Good use of..."],
"improvements": ["Could elaborate more on...", "Consider mentioning..."],
"suggestion": "To strengthen this answer, try using the STAR method..."
}
}`;POST /api/sessions
Content-Type: application/json
{
"roleTitle": "Senior FullStack .NET/Angular Developer",
"companyName": "Google", // Optional
"jobDescription": "We are looking for a Senior FullStack Developer..."
}Response:
{
"id": "sess_abc123",
"roleTitle": "Senior FullStack .NET/Angular Developer",
"companyName": "Google",
"seniorityLevel": "senior",
"createdAt": "2024-12-14T10:00:00Z",
"status": "in-progress"
}GET /api/sessions/{id}Response:
{
"success": true,
"data": {
"session": {
"id": "sess_abc123",
"roleTitle": "Senior FullStack .NET/Angular Developer",
"status": "in-progress"
},
"questions": [
{
"id": 1,
"question": "Describe how you would architect...",
"difficulty": "senior",
"category": "system-design",
"timeLimit": 600
}
]
}
}Note:
timeLimitis in seconds. System Design questions get up to 600s (10 min) to allow for comprehensive architectural explanations, while Technical questions typically get 60-240s (1-4 min).
GET /api/speech/tokenResponse:
{
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJS...",
"region": "eastus",
"expiresAt": "2024-12-14T10:10:00Z"
}POST /api/evaluate
Content-Type: application/json{
"sessionId": "sess_abc123",
"responses": [
{
"questionId": 1,
"transcription": "For a high-traffic e-commerce platform, I would design..."
}
]
}Response:
{
"sessionId": "sess_abc123",
"overallScore": 78,
"performanceBand": "good",
"evaluations": [
{
"questionId": 1,
"scores": {
"relevance": 85,
"technicalAccuracy": 82,
"clarity": 75,
"depth": 78,
"structure": 70,
"confidence": 80
},
"overallScore": 79,
"feedback": {
"strengths": ["Good understanding of microservices patterns"],
"improvements": ["Could mention specific .NET libraries"],
"suggestion": "Consider discussing containerization with Docker/K8s"
}
}
]
}Recordings use a write-ahead path, online or offline, so failed uploads retain a local copy for retry. Browser storage deletion, quota limits, or unavailable IndexedDB can still prevent durable storage; the direct-upload fallback is not persisted:
record β save to IndexedDB (pending) β upload β server confirms answer + audio β delete local copy
β
ββ failure β retry with backoff (5s Β· 3βΏ, capped at 5 min)
| Piece | File | Responsibility |
|---|---|---|
| Storage | src/lib/offline-storage.ts |
IndexedDB ai-interview-offline v2. Atomic claim/lease/complete/fail transitions; v1 records are migrated in place on first open. |
| Engine | src/lib/response-sync.ts |
Claims due records and uploads them. Audio acknowledgement must match SHA-256, or byte size when Web Crypto is unavailable; a superseded recording can also be removed. |
| Coordinator | src/lib/sync-controller.ts |
One per tab. Leader election with navigator.locks, BroadcastChannel notifications, scheduler that wakes exactly when the next retry or lease expiry is due. |
| Server | src/lib/response-submission.ts, src/app/api/responses/sync/route.ts |
Idempotent, order-aware upsert. Audio is stored in question_responses.audio_blob with MIME type, size and checksum. |
| UI | useOfflineSupport, OfflineStatusIndicator, ResponseSyncBadge, PendingUploadsNotice |
Real per-answer status: saved on device β uploading β saved / retrying (countdown) / needs attention (retry Β· discard). |
Policies
- Idempotency β each recording gets a
clientResponseId(UUID) when it is recorded. A retry after a lost acknowledgement returnsduplicateand rewrites nothing. - Ordering β each recording carries
recordedAt. An older recording arriving after a newer one returnssupersededand never overwrites it. Rows written by older builds (norecordedAt) are compared bycompletedAt. - Replacement β a newer recording (
updated) replaces the answer and audio and deletes the stale evaluation so the results page re-evaluates it. - Audio integrity β the client sends
audioSha256; the server verifies it (400 AUDIO_CHECKSUM_MISMATCHβ retried) and echoes the stored checksum back. Uploads above 25 MB are rejected (413). - Retries β network errors, 5xx, 408 and 429 retry forever with capped backoff; other 4xx (
SESSION_NOT_FOUND,QUESTION_NOT_FOUND, β¦) mark the record rejected and wait for the user to retry or discard. After 5 failed attempts the UI flags the answer as needing attention. - Recovery β a record claimed by a tab that closed or crashed mid-upload (expired lease, 60 s) is reclaimed by the next run. Atomic IndexedDB claims prevent simultaneous claims while a lease is valid; an expired lease can produce a repeated upload, which server idempotency handles.
- Concurrency on the server β submissions for the same question are serialised with a PostgreSQL advisory lock inside one transaction.
- Reload β the interview room merges local records for the session, so an answer saved on the device still shows as answered after a reload (the page itself still needs the network to load).
POST /api/responses/sync (multipart): questionId, sessionId, durationSeconds, transcription?, audio?, clientResponseId?, recordedAt? (unix ms), audioSha256?, audioMimeType?, recordedOffline?.
Response 201 created / 200 updated | duplicate | superseded:
{
"success": true,
"data": {
"outcome": "created",
"responseId": "β¦",
"syncedAt": "β¦",
"audio": { "stored": true, "bytes": 48213, "sha256": "β¦" }
}
}| Command | What it runs | Needs |
|---|---|---|
pnpm test |
Mocked checks β storage/migration, engine, tab coordination, submission policy, route handler (Prisma mocked), hook and components (jsdom). 100+ tests. | nothing (fake IndexedDB, stubbed fetch) |
pnpm test:integration |
Real PostgreSQL β repeat submissions, ordering, audio bytes/SHA-256 round-trip, concurrent retries, route end-to-end. Skipped with a warning when DATABASE_URL is unset. |
DATABASE_URL + prisma db push |
pnpm test:all |
both | |
pnpm typecheck / pnpm lint / pnpm build |
static checks and production build | prisma generate |
pnpm test # no services required
# Set DATABASE_URL to a dedicated test database in your shell first.
pnpm exec prisma db push
pnpm test:integrationNo test talks to Azure OpenAI or Azure Speech; the integration suite creates its own session, runs, and deletes it.
Model: gpt-5.4-mini (version 2026-03-17), Global Standard.
Use the current Azure OpenAI pricing for your region, currency, and agreement. Estimate input, cached input, output, and reasoning usage; previous GPT-4o-mini estimates do not apply.
Terraform defaults to 30 capacity units. TPM/RPM conversion and quota are model- and subscription-specific; verify them before deployment rather than assuming 30K TPM or 3,000 requests/minute.
Default SKU: S0 (Standard) - Pay-as-you-go
| Feature | Free Tier (F0) | Standard Tier (S0) Price |
|---|---|---|
| Text-to-Speech (Neural) | 500K characters/month | $16 / 1M characters |
| Text-to-Speech (Neural HD) | N/A | $28 / 1M characters |
| Speech-to-Text (Standard) | 5 hours/month | $1 / hour (real-time) |
| Speech-to-Text (Custom) | N/A | $1.40 / hour (real-time) |
Example Usage:
- 100 questions β 50K TTS characters β $0.80 (or free with F0)
- 100 responses β 2 hours STT β $2.00 (or free with F0)
Calculate totals from measured GPT-5.4-mini token usage plus Speech consumption and state storage. A session-count-only estimate is insufficient because reasoning and generated question counts vary.
- Use F0 (Free) tier for Speech Service during development (500K TTS chars + 5 hours STT/month)
- Reuse stable system prompts to benefit from model-supported prompt caching; verify the current cached-input price
- Use dynamic quota for batch deployments to optimize capacity usage
- Implement token counting and set usage limits per user/session
- Cache topic extraction results for similar job descriptions
- Monitor usage through Azure Portal > Cost Management + Billing
Comprehensive documentation is available in the docs/ directory:
- π Architecture Diagrams - System architecture and interview flow visualizations
- πΈ Screenshots - UI walkthrough showing setup, interview, and results pages
- π Lessons Learned - Technical challenges and solutions from building the platform
- ποΈ Infrastructure Guide - Terraform deployment guide for Azure resources
Quick Links:
- View All Diagrams & Screenshots
- Lessons Learned - Real Technical Challenges
- Infrastructure Setup Guide
- Project architecture design
- Next.js 16 project setup with TypeScript
- Session creation with role & job description (mandatory fields)
- Azure OpenAI integration for question generation
- Seniority-aligned question logic
- Countdown timer per question (1-10 min based on category)
- Azure TTS for reading questions
- Azure STT for recording responses with auto-stop
- Basic evaluation with scoring
- Results display
- Audio waveform visualization
- Response playback feature
- Detailed feedback with charts
- Export results as PDF
- Dark/light theme support
- Pause/resume recording
- Multiple interview modes (behavioral, coding, system design)
- Practice history and progress tracking
- Custom question banks
- Real-time coaching during responses
- Multi-language support
- Team/enterprise features
Contributions are welcome! Please follow these steps:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
pnpm test
pnpm typecheck
pnpm lint
pnpm buildpnpm test runs unit tests for IndexedDB storage, the response sync engine, the cross-tab sync controller, per-question status helpers, React sync components, server-side submission policy, and the sync route. IndexedDB and database writes are mocked; these tests do not access PostgreSQL or Azure. Controller tests use injected locks, channels, network events, and timers. React tests use jsdom and Testing Library with mocked network requests. Use pnpm test:watch during development.
vitest.config.mts also defines an integration project (pnpm test:integration) and a combined runner (pnpm test:all). PostgreSQL integration tests exercise audio persistence, repeated submissions, ordering, and concurrent retries. Set DATABASE_URL to a dedicated test database with the current schema before running them. The integration setup loads .env through dotenv/config, not .env.local; tests are skipped if no connection is configured. Tests create temporary sessions and remove their data afterward.
Before using this phase against an existing database, review and apply 001_offline_sync_columns.sql to the intended database. It adds five nullable columns and a unique index without dropping existing data. Applying this SQL does not create the rest of the application's schema. Regenerate the local Prisma client afterward:
pnpm exec prisma generateThe local response queue uses schema version 2, migrates existing recordings, leases uploads across tabs, and schedules retriable failures with exponential backoff. Terminal failures remain stored for explicit retry or discard.
SyncController adds startup recovery, automatic retry timers, network and visibility triggers, Web Locks coordination, and BroadcastChannel notifications. Without Web Locks, atomic IndexedDB claims still prevent simultaneous claims of the same recording. It exposes queue summaries and explicit retry/discard actions without including audio blobs in observable state. Call start() to activate coordination and stop() to remove listeners and timers.
useOfflineSupport now subscribes to the shared controller and starts coordination when mounted. InterviewRoom saves every answer locally before uploading, restores pending recordings into per-question state, and displays upload/retry status. If local storage fails, it attempts one direct upload; this fallback cannot persist or automatically retry the recording.
OfflineStatusIndicator exposes queue details, retry, and confirmed discard actions. PendingUploadsNotice warns when a results page has locally stored answers that the server has not confirmed. Reload results after uploads finish to include those answers. Automated checks cover the hook and status components with mocked browser dependencies, not a live microphone or Azure Speech session.
The sync endpoint persists audio bytes and their MIME type, size, and SHA-256 in PostgreSQL. It serializes submissions for the same question and session with a transaction-scoped advisory lock. clientResponseId identifies retries, recordedAt prevents older recordings from overwriting newer ones, and replacing an answer invalidates its old evaluation. The client deletes its local copy only after a confirmed acknowledgement with matching audio checksum (or size when Web Crypto is unavailable), or when the server reports that a newer answer supersedes it. The response retains responseId and syncedAt and adds outcome and audio confirmation metadata.
- Follow TypeScript best practices
- Use Server Components where possible
- Write meaningful commit messages
- Add tests for new features
- Update documentation as needed
This project is licensed under the MIT License - see the LICENSE file for details.
- Microsoft Azure AI Services for powerful AI capabilities
- Next.js for the amazing React framework
- Vercel for the Geist font family
- shadcn/ui for beautiful UI components
- Tailwind CSS v4 for utility-first styling
Built with β€οΈ using Azure AI Services and Next.js 16






