An AI-powered interview assistant that represents Sanath Kumar J S in technical interviews using a personal knowledge base, multi-signal RAG search, real-time voice interaction, and a live job market skill gap analyzer.
Built with Next.js 14 Β· .NET 8 Β· PostgreSQL + pgvector Β· Groq LLM Β· HuggingFace Embeddings Β· Groq Whisper STT
- π§ 3-signal RAG search β Body embedding + title embedding + AI-generated question variants per chunk
- ποΈ Voice input β Interviewer asks questions via microphone (Groq Whisper STT)
- π Voice playback β Answers read aloud via browser TTS (Web Speech API)
- π Unanswered question tracking β Questions outside KB stored for prep
- π Prep dashboard β Review, answer, and promote unanswered questions to KB
- π Prepare for interview β Read all KB Q&A in a clean study mode with AI-generated interview angles per section
- π Session history β Browse past interviews with full transcripts and per-message feedback
- π Confidence scoring β Every answer shows confidence % from weighted vector similarity
- π Security β Prompt injection detection, system prompt protection, persona guard
- π― Skill Gap Analyzer β Live job search from Adzuna + Remotive, skill matching, salary insights, company rankings, auto-digest toggle
- π¨ 5 chat themes β Obsidian (default), Monolith, Counsel, Signal (light mode), Slate Editorial β selectable from homepage
- π‘οΈ Fallback database β Automatic failover from Supabase to Neon when primary DB is unreachable
- π± Mobile responsive β Works on all screen sizes
βββββββββββββββββββββββ HTTP ββββββββββββββββββββββββ
β Next.js 14 UI β ββββββββββββ β .NET 8 Web API β
β (Vercel) β β (Render.com) β
βββββββββββββββββββββββ ββββββββββββ¬ββββββββββββ
β
βββββββββββββββββββββββββββββΌβββββββββββββββββββββββ
β β β
βββββββββββΌβββββββ ββββββββββββΌββββββ ββββββββββΌβββββββ
β PostgreSQL 16 β β Groq Cloud β β HuggingFace β
β + pgvector β β LLM + STT β β Embeddings β
β Primary: β β (Free tier) β β (Free tier) β
β Supabase β ββββββββββββββββββ βββββββββββββββββ
β Fallback: β
β Neon β ββββββββββββββββββ ββββββββββββββββββ
ββββββββββββββββββ β Adzuna API β β Remotive API β
β (Job search) β β (Remote jobs) β
ββββββββββββββββββ ββββββββββββββββββ
The DatabaseConnectionManager singleton manages automatic failover:
- Primary: Supabase (may pause after 7 days of inactivity on free tier)
- Fallback: Neon (serverless, never pauses)
- On primary failure, switches to fallback for 5 minutes, then retries primary
/pingrunsSELECT 1to keep both Render and the DB alive/healthreturns which DB is active:{"status":"healthy","db":"primary (Supabase)"}
interview-bot/
βββ interview-bot-ui/ # Next.js 14 frontend
β βββ app/
β β βββ page.tsx # Home page + theme picker
β β βββ chat/page.tsx # Chat interface (theme-aware)
β β βββ prep/page.tsx # Prep dashboard (PIN protected)
β β βββ prepare/page.tsx # KB reading mode for interview prep
β β βββ skill-gap/page.tsx # Skill Gap Analyzer
β β βββ sessions/
β β βββ page.tsx # Session list
β β βββ [id]/page.tsx # Transcript view with feedback
β βββ components/
β β βββ Navbar.tsx # Admin nav bar (theme-aware)
β β βββ ThemeProvider.tsx # React context for chat themes
β β βββ chat/
β β βββ InputBar.tsx # (theme-aware)
β β βββ MessageBubble.tsx # (theme-aware)
β β βββ TypingIndicator.tsx # (theme-aware)
β βββ lib/
β βββ api.ts # All fetch wrappers
β βββ themes.ts # 5 chat theme definitions
β
βββ interview-bot-api/ # .NET 8 Web API
β βββ Controllers/
β β βββ ChatController.cs
β β βββ KnowledgeController.cs # KB file list + chunk reader
β β βββ TranscribeController.cs
β β βββ IngestionController.cs
β β βββ SkillGapController.cs
β βββ Services/
β β βββ ChatService.cs
β β βββ KnowledgeSearchService.cs
β β βββ IngestionService.cs
β β βββ EmbeddingService.cs
β β βββ DatabaseConnectionManager.cs # Primary/fallback DB failover
β β βββ ChunkMetadataHelper.cs
β β βββ SkillGapService.cs
β βββ Models/
β β βββ ChatModels.cs
β β βββ KnowledgeChunk.cs
β β βββ SkillGapModels.cs
β βββ knowledge-base/ # Personal KB β .md files
β β βββ introduction.md
β β βββ career-journey.md
β β βββ ai-rag.md
β β βββ dotnet.md
β β βββ dotnet-interview-qa.md
β β βββ ...
β βββ Dockerfile # Used by Render.com for deployment
β
βββ docs/
βββ schema.sql # Core DB schema
βββ skill_gap_migration.sql # Skill Gap tables migration
βββ RENDER_MIGRATION.md # Step-by-step Render.com setup guide
Question asked
β
HuggingFace BAAI/bge-base-en-v1.5 β embed query (768d)
β
3 parallel SQL queries (all use HNSW index):
Signal 1: top 15 by body_embedding <=> queryVec
Signal 2: top 15 by title_embedding <=> queryVec
Signal 3: top 15 by questions_embedding <=> queryVec
β
Merge all candidates by chunk_id in C#
β
Compute weighted score per chunk:
titleWeight = title_word_count >= 5 ? 0.30 : 0.15
bodyWeight = title_word_count >= 5 ? 0.25 : 0.40
finalScore = (questionsSim Γ 0.45)
+ (titleSim Γ titleWeight)
+ (bodySim Γ bodyWeight)
β
Tag overlap soft boost (+0.05) + file keyword boost (+0.05β0.08)
β
Confidence gate:
β₯ 0.62 β HIGH β Answer from KB
β₯ 0.52 β MEDIUM β Answer from KB
< 0.52 β LOW β Store as unanswered question
β
Groq llama-3.3-70b-versatile generates answer
β
Save to chat_messages with confidence score
| Signal | Weight | Purpose |
|---|---|---|
questions_embedding |
0.45 | 5 AI-generated question variants per chunk at ingest time. QβQ matching is the most accurate signal β solves the QβA vector space mismatch. |
title_embedding |
0.30 / 0.15 | Section heading embedded alone. Adaptive: 5+ word headings get 0.30; short headings get 0.15. |
body_embedding |
0.25 / 0.40 | Semantic content of the full chunk. Safety net for paraphrasing. Gets higher weight when title is short. |
Live job market analysis comparing Sanath's profile against current job postings.
User enters role keywords + location
β
POST /api/skill-gap
β
Adzuna API (IN β GB β US fallback) + Remotive (remote) fetched in parallel
β
Groq LLM extracts required / nice-to-have / trending skills from JDs in batches
β
Compare extracted skills against Sanath's KB-derived skill profile
β
Score each job:
ATS Score = keyword match % (Sanath's skills vs JD text)
Match Score = required skill overlap %
β
Return:
- Ranked job listings with ATS + match scores
- Matched skills β
| Missing skills β | Trending skills π₯
- Salary range (min / median / max from Adzuna data)
- Top hiring companies ranked by job count
β
Jobs persisted to DB (job_listings table)
User can save jobs β tracked in job_applications table
Auto-digest toggle β settings stored in user_settings table
| Method | Path | Description |
|---|---|---|
| POST | /api/skill-gap |
Run full analysis β fetch jobs + gap report |
| POST | /api/skill-gap/save-job |
Save / update job application status |
| GET | /api/skill-gap/saved-jobs |
List all saved + tracked jobs |
| GET | /api/skill-gap/settings |
Get user settings (auto-digest, keywords) |
| POST | /api/skill-gap/settings |
Update user settings |
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE knowledge_chunks (
id SERIAL PRIMARY KEY,
source_file TEXT,
section_title TEXT,
chunk_text TEXT,
chunk_index INTEGER,
embedding VECTOR(768),
topic TEXT,
tags TEXT[],
hit_count INTEGER DEFAULT 0,
last_used_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW(),
title_embedding VECTOR(768),
questions_embedding VECTOR(768),
questions_text TEXT[],
title_word_count INT
);
CREATE TABLE interview_sessions (
id SERIAL PRIMARY KEY,
session_code TEXT UNIQUE,
company_name TEXT,
interviewer_name TEXT,
round_number INTEGER,
started_at TIMESTAMPTZ DEFAULT NOW(),
ended_at TIMESTAMPTZ,
status TEXT DEFAULT 'active',
overall_rating SMALLINT,
notes TEXT
);
CREATE TABLE chat_messages (
id SERIAL PRIMARY KEY,
session_id INTEGER REFERENCES interview_sessions(id),
sequence_number INTEGER,
role TEXT,
message_text TEXT,
confidence_score FLOAT,
answer_source TEXT,
fallback_provider TEXT,
response_time_ms INTEGER,
was_helpful BOOLEAN,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE unanswered_questions (
id SERIAL PRIMARY KEY,
session_id INTEGER REFERENCES interview_sessions(id),
message_id INTEGER REFERENCES chat_messages(id),
question_text TEXT,
question_embedding VECTOR(768),
times_asked INTEGER DEFAULT 1,
status TEXT DEFAULT 'new',
priority TEXT DEFAULT 'low',
sanath_answer TEXT,
kb_chunk_id INTEGER REFERENCES knowledge_chunks(id),
question_category TEXT,
sanath_answered_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ,
first_asked_at TIMESTAMPTZ DEFAULT NOW(),
last_asked_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE session_analytics (
id SERIAL PRIMARY KEY,
session_id INTEGER REFERENCES interview_sessions(id) UNIQUE,
total_questions INTEGER DEFAULT 0,
answered_from_kb INTEGER DEFAULT 0,
unanswered_count INTEGER DEFAULT 0,
avg_confidence_score FLOAT,
top_kb_files JSONB,
weak_topic_areas JSONB,
duration_minutes INTEGER
);
-- Skill Gap tables (run docs/skill_gap_migration.sql)
CREATE TABLE job_listings ( ... );
CREATE TABLE job_applications ( ... );
CREATE TABLE user_settings ( ... );
-- HNSW indexes
CREATE INDEX ON knowledge_chunks USING hnsw (embedding vector_cosine_ops);
CREATE INDEX ON knowledge_chunks USING hnsw (title_embedding vector_cosine_ops);
CREATE INDEX ON knowledge_chunks USING hnsw (questions_embedding vector_cosine_ops);14 .md files in interview-bot-api/knowledge-base/. answering-guidelines.md excluded from search.
| File | Chunks | Topic |
|---|---|---|
introduction.md |
7 | Who I am, career summary, specialization |
career-journey.md |
5 | Toyota Tsusho, Capgemini, Euromonitor, Ingenio |
recent-project.md |
11 | RAG systems, JWT migration, integrations |
ai-rag.md |
13 | 3 production RAG pipelines, tech decisions |
dotnet-interview-qa.md |
20 | 20 Q&A pairs: DI, async/await, GC, EF Core, Polly |
dotnet.md |
6 | Years, strongest areas, design patterns |
leadership.md |
4 | 3 leadership stories + philosophy |
general-hr.md |
9 | Strengths, weakness, salary, notice, education |
my-approach.md |
7 | System design examples |
arrays-strings.md |
7 | DSA: two pointers, sliding window, hashmap |
trees.md |
6 | BST, traversal, DFS vs BFS |
dynamic-programming.md |
4 | DP patterns and approach |
complexity-cheatsheet.md |
4 | Big-O reference |
answering-guidelines.md |
β | EXCLUDED β internal prompt instructions |
Total indexed: 101 chunks across 13 files
# 1. Clone
git clone https://github.com/sanathjs/interview-bot.git
cd interview-bot
# 2. Backend
cd interview-bot-api
cp appsettings.example.json appsettings.json
dotnet restore && dotnet run # http://localhost:5267
# 3. Frontend (new terminal)
cd interview-bot-ui
cp .env.example .env.local
npm install && npm run dev # http://localhost:3000
# 4. Ingest KB
curl -X POST http://localhost:5267/api/ingest -H "X-Admin-Key: your-key"{
"DATABASE_URL": "Host=localhost;...",
"FALLBACK_DATABASE_URL": "Host=ep-xxx.neon.tech;Port=5432;Database=neondb;Username=...;Password=...;SSL Mode=Require;Trust Server Certificate=true",
"ADMIN_INGEST_KEY": "your-secret-key",
"LlmProvider": "groq",
"HuggingFace": { "ApiKey": "hf_..." },
"Groq": {
"ApiKey": "gsk_...",
"Model": "llama-3.3-70b-versatile",
"BaseUrl": "https://api.groq.com/openai/v1"
},
"Adzuna": {
"AppId": "your_adzuna_app_id",
"AppKey": "your_adzuna_app_key"
}
}NEXT_PUBLIC_API_URL=http://localhost:5267
NEXT_PUBLIC_PREP_PIN=1234DATABASE_URL = (Supabase pooler connection string)
FALLBACK_DATABASE_URL = (Neon connection string β optional but recommended)
ADMIN_INGEST_KEY = your-secret-key
LlmProvider = groq
Groq__ApiKey = gsk_...
Groq__Model = llama-3.3-70b-versatile
Groq__BaseUrl = https://api.groq.com/openai/v1
HuggingFace__ApiKey = hf_...
Adzuna__AppId = your_adzuna_app_id
Adzuna__AppKey = your_adzuna_app_key
ASPNETCORE_URLS = http://+:10000
PORT = 10000
Render Docker services use port 10000 by default.
NEXT_PUBLIC_API_URL = https://your-app.onrender.com
NEXT_PUBLIC_PREP_PIN = your-pin
| Service | Purpose | Get it at | Cost |
|---|---|---|---|
| Groq | LLM chat + Whisper STT | console.groq.com | Free |
| HuggingFace | BAAI/bge-base-en-v1.5 embeddings | huggingface.co/settings/tokens | Free |
| Adzuna | Job search API (India + global fallback) | developer.adzuna.com | Free (1000 calls/day) |
| Remotive | Remote job search | remotive.com/api | Free, no key needed |
| Method | Path | Description | Auth |
|---|---|---|---|
| POST | /api/chat |
RAG chat β answer + confidence + sources + botSequenceNumber | None |
| GET | /api/sessions |
List sessions with stats | None |
| GET | /api/sessions/{id}/detail |
Full transcript with per-message feedback (was_helpful) | None |
| PATCH | /api/sessions/{code}/details |
Update interviewer name + company (UPSERT) | None |
| POST | /api/sessions/{code}/end |
End session | None |
| PATCH | /api/messages/{seqNum}/feedback |
Save thumbs up/down (by DB sequence_number) | None |
| GET | /api/unanswered |
Prep dashboard questions | None |
| PATCH | /api/unanswered/{id}/answer |
Save answer | None |
| POST | /api/unanswered/{id}/promote |
Add answer to KB | None |
| DELETE | /api/unanswered/{id} |
Delete question | None |
| POST | /api/transcribe |
Audio β Groq Whisper β text | None |
| POST | /api/ingest |
Re-ingest all KB files | X-Admin-Key header |
| GET | /ping |
Keep-alive β runs SELECT 1 to prevent Supabase pause | None |
| GET | /health |
Diagnostic β shows active DB (primary/fallback) | None |
| Method | Path | Description | Auth |
|---|---|---|---|
| GET | /api/knowledge/files |
List all KB files with display names + chunk counts | None |
| GET | /api/knowledge/files/{file} |
All sections from a file β title, body, question variants | None |
| Method | Path | Description | Auth |
|---|---|---|---|
| POST | /api/skill-gap |
Fetch jobs + run full gap analysis | None |
| POST | /api/skill-gap/save-job |
Save / update job application status | None |
| GET | /api/skill-gap/saved-jobs |
List all saved + tracked jobs | None |
| GET | /api/skill-gap/settings |
Get user settings | None |
| POST | /api/skill-gap/settings |
Update user settings | None |
| Layer | Platform | Cost | Notes |
|---|---|---|---|
| Frontend | Vercel | Free forever | Auto-deploys on git push |
| Backend | Render.com | Free forever | Spins down after 15 min idle |
| Database (primary) | Supabase | Free forever | 500MB limit, pauses after 7 days idle |
| Database (fallback) | Neon | Free forever | Serverless, never pauses, auto-failover |
| Embeddings | HuggingFace | Free forever | |
| LLM + STT | Groq | Free forever | |
| Job Search | Adzuna + Remotive | Free forever | 1000 calls/day |
| Keep-alive | cron-job.org | Free forever | Pings /health every 10 min |
| Total | $0/month |
Cold start: Render free tier spins down after 15 min idle. First request takes ~30s. Set up a cron-job.org ping to
https://your-app.onrender.com/healthevery 10 minutes to prevent this completely. The/healthendpoint runsSELECT 1which also keeps Supabase from pausing.
| Layer | Platform | Cost |
|---|---|---|
| Frontend | Vercel | Free |
| Backend | Render Starter | ~$7/mo (always on) |
| Database | Supabase Pro | ~$25/mo |
| Embeddings | OpenAI text-embedding-3-small | ~$0.01/mo |
| LLM | gpt-4o-mini | ~$1β3/mo |
# 1. Run Skill Gap migration in Supabase SQL Editor
# Copy contents of docs/skill_gap_migration.sql β run in Supabase
# 2. Set up Render (see docs/RENDER_MIGRATION.md for full steps)
# 3. Push code β Render + Vercel auto-deploy on push
git add .
git commit -m "your message"
git push origin main
# 4. Re-ingest KB after any .md changes
curl -X POST https://your-app.onrender.com/api/ingest \
-H "X-Admin-Key: your-key"
# 5. Update NEXT_PUBLIC_API_URL in Vercel β Settings β Environment Variables
# Set to your Render URL- DatabaseConnectionManager (singleton) wraps all DB access with automatic primaryβfallback switching and 5-min retry cooldown
FALLBACK_DATABASE_URLis optional β if not set, app works with primary only (no failover)/pingrunsSELECT 1viaDatabaseConnectionManager.ProbeAsync()β keeps both Render and Supabase alive/healthreturns JSON withstatus+db(which database is active)UpdateSessionDetailsAsyncuses UPSERT β creates session row if PATCH arrives before first chat messagesession_analyticshas no auto-trigger β stats fall back to live subqueries fromchat_messages
ChatResponseincludesbotSequenceNumberβ the DB sequence_number of the bot's reply- Frontend passes
sequenceNumber(not Reactmsg-NIDs) toPATCH /api/messages/{seqNum}/feedback GetTranscriptByIdAsyncreturnswas_helpfulper message β shows feedback in session transcript- Prompt injection blocked pre-LLM via
IsPromptInjection()β 24 phrases detected
- 5 chat themes defined in
lib/themes.ts: Obsidian, Monolith, Counsel, Signal (light), Slate Editorial ThemeProvider(React context) wraps the app; theme saved inlocalStorageasib_theme- All chat components use
useTheme()hook βconst C = useTheme()replaces hardcoded color objects - Theme picker accessible from the palette icon (top-right of homepage)
answering-guidelines.mdexcluded from search at ingest time β do not rename it- Re-ingest required after any KB change β ~5 min for 101 chunks
- KB headings should be written as the exact question an interviewer would ask
/api/knowledge/filesreturns all files with display names (e.g.,dotnet-interview-qa.mdβ "DotNet Interview Q&A")/api/knowledge/files/{file}strips the "Topic: X\nSection: Y\n\n" prefix from chunk_text for clean reading
- Voice input:
MediaRecorderβ Groq Whisper β auto-sends transcribed text - Voice playback:
window.speechSynthesis(browser TTS, no API needed)
- Prep dashboard is PIN-protected via
NEXT_PUBLIC_PREP_PINenv variable - Admin gear icon visible only when
localStorage.ib_role === "admin" - Prepare tab visible in admin nav and admin dashboard homepage
- Adzuna fetches IN β GB β US; falls back to international if India returns fewer than 10 jobs
job_listings.external_idis unique β re-running analysis updates scores, no duplicates- Render uses port
10000β setASPNETCORE_URLS=http://+:10000andPORT=10000 - Keep Render warm: cron-job.org pings
/healthevery 10 minutes
5 selectable themes β user picks from the palette icon on the homepage. Choice persists in localStorage.
| Theme | Palette | Mode | Vibe |
|---|---|---|---|
| Obsidian (default) | Amber on deep black | Dark | Warm, the original |
| Monolith | Platinum + Electric Blue | Dark | Cold intellect, zero noise |
| Counsel | Charcoal + Gold, Serif | Dark | Senior partner's office, gravitas |
| Signal | Navy + White + Green | Light | Trust-first, corporate-safe |
| Slate Editorial | Near-Black + Coral | Dark | Editorial confidence, shipped product feel |
Themes affect: chat background, message bubbles, avatars, input bar, typing indicator, navbar, follow-up buttons, source banners, bold/list text highlighting, and the live dot color.
- Upload PDF resume β ingest into KB as
resume.md -
POST /api/skill-gap/resumeβ Groq tailors resume to JD β.docxdownload -
POST /api/skill-gap/coverβ Groq writes cover letter β.docxdownload - ATS score with keyword breakdown per job
- Job application status board (saved β applied β interview β offer)
-
IHostedServicedaily background job at 9am - Auto-digest β top 10 matched jobs saved daily when enabled
- End session UI β star rating + notes modal
- Analytics dashboard β KB hit rate, weak topics, confidence trends
- ElevenLabs voice cloning (~$6/mo)
- Upgrade embeddings to OpenAI
text-embedding-3-small
appsettings.jsonand.env.localgitignored- Old exposed keys rotated; git history purged via
filter-branch appsettings.example.jsonand.env.examplehave no real values- Prep dashboard PIN-protected via env variable
- API endpoints have no auth currently β add JWT for production
- Prompt injection blocked pre-LLM: 24 phrases in
ChatService.cs - System prompt never revealed β LLM-level security rules in Groq system message
MIT β feel free to fork and adapt for your own interview assistant.
Built with β€οΈ by Sanath Kumar J S