https://github.com/safdarali01/ai-interview-agent
Get Interview-Ready with AI-Powered Practice & Feedback. Practice real interview questions & get instant feedback
https://github.com/safdarali01/ai-interview-agent
firebase gemini nextjs shadcn-ui tailwindcss vapi-ai zod
Last synced: about 2 months ago
JSON representation
Get Interview-Ready with AI-Powered Practice & Feedback. Practice real interview questions & get instant feedback
- Host: GitHub
- URL: https://github.com/safdarali01/ai-interview-agent
- Owner: safdarali01
- Created: 2025-06-27T11:14:15.000Z (11 months ago)
- Default Branch: main
- Last Pushed: 2025-07-18T14:34:28.000Z (11 months ago)
- Last Synced: 2026-01-03T17:14:35.878Z (5 months ago)
- Topics: firebase, gemini, nextjs, shadcn-ui, tailwindcss, vapi-ai, zod
- Language: TypeScript
- Homepage: https://prepview-alpha.vercel.app
- Size: 1.24 MB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
PrepView: A job interview preparation platform powered by Vapi AI Voice agents
The objective of this project is to develop Prepview, an AI-enabled platform that utilizes voice agents to give personalized, real-time feedback that will helpβusers better prepare for job interviews. The platform will enable users to do mock interviews in an effective,βscalable, yet interactive manner.
## π Table of Contents
1. π€ [Introduction](#introduction)
2. βοΈ [Tech Stack](#tech-stack)
3. π [Features](#features)
4. π€Έ [Quick Start](#quick-start)
5. πΈοΈ [Snippets (Code to Copy)](#snippets)
6. π [Demo](#demo)
7. π [About Me](#about)
7. πββοΈ [Support Me](#support)
Built with Next.js for the user interface and backend logic, Firebase for authentication and data storage, styled with TailwindCSS and using Vapi's voice agents, PrepView is a website project designed to help you learn integrating AI models with your apps. The platform offers a sleek and modern experience for job interview preparation.
- Next.js
- Firebase
- Tailwind CSS
- Vapi AI
- shadcn/ui
- Google Gemeni
- Zod
π **Authentication**: Sign Up and Sign In using password/email authentication handled by Firebase.
π **Create Interviews**: Easily generate job interviews with help of Vapi voice assistants and Google Gemini.
π **Get feedback from AI**: Take the interview with AI voice agent, and receive instant feedback based on your conversation.
π **Modern UI/UX**: A sleek and user-friendly interface designed for a great experience.
π **Interview Page**: Conduct AI-driven interviews with real-time feedback and detailed transcripts.
π **Dashboard**: Manage and track all your interviews with easy navigation.
π **Responsiveness**: Fully responsive design that works seamlessly across devices.
and many more, including code architecture and reusability
Follow these steps to set up the project locally on your machine.
**Prerequisites**
Make sure you have the following installed on your machine:
- [Git](https://git-scm.com/)
- [Node.js](https://nodejs.org/en)
- [npm](https://www.npmjs.com/) (Node Package Manager)
**Cloning the Repository**
```bash
git clone https://github.com/safdarali01/AI-Interview-Agent.git
cd ai-interview-agent
```
**Installation**
Install the project dependencies using npm:
```bash
npm install
```
**Set Up Environment Variables**
Create a new file named `.env.local` in the root of your project and add the following content:
```env
NEXT_PUBLIC_VAPI_WEB_TOKEN=
NEXT_PUBLIC_VAPI_WORKFLOW_ID=
GOOGLE_GENERATIVE_AI_API_KEY=
NEXT_PUBLIC_BASE_URL=
NEXT_PUBLIC_FIREBASE_API_KEY=
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=
NEXT_PUBLIC_FIREBASE_PROJECT_ID=
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=
NEXT_PUBLIC_FIREBASE_APP_ID=
FIREBASE_PROJECT_ID=
FIREBASE_CLIENT_EMAIL=
FIREBASE_PRIVATE_KEY=
```
Replace the placeholder values with your actual **[Firebase](https://firebase.google.com/)**, **[Vapi](https://vapi.ai/)** credentials.
**Running the Project**
```bash
npm run dev
```
Open [http://localhost:3000](http://localhost:3000) in your browser to view the project.
globals.css
```css
@import "tailwindcss";
@plugin "tailwindcss-animate";
@custom-variant dark (&:is(.dark *));
@theme {
--color-success-100: #49de50;
--color-success-200: #42c748;
--color-destructive-100: #f75353;
--color-destructive-200: #c44141;
--color-primary-100: #dddfff;
--color-primary-200: #cac5fe;
--color-light-100: #d6e0ff;
--color-light-400: #6870a6;
--color-light-600: #4f557d;
--color-light-800: #24273a;
--color-dark-100: #020408;
--color-dark-200: #27282f;
--color-dark-300: #242633;
--font-mona-sans: "Mona Sans", sans-serif;
--bg-pattern: url("/pattern.png");
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: var(--light-100);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
p {
@apply text-light-100;
}
h2 {
@apply text-3xl font-semibold;
}
h3 {
@apply text-2xl font-semibold;
}
ul {
@apply list-disc list-inside;
}
li {
@apply text-light-100;
}
}
@layer components {
.btn-call {
@apply inline-block px-7 py-3 font-bold text-sm leading-5 text-white transition-colors duration-150 bg-success-100 border border-transparent rounded-full shadow-sm focus:outline-none focus:shadow-2xl active:bg-success-200 hover:bg-success-200 min-w-28 cursor-pointer items-center justify-center overflow-visible;
.span {
@apply bg-success-100 h-[85%] w-[65%];
}
}
.btn-disconnect {
@apply inline-block px-7 py-3 text-sm font-bold leading-5 text-white transition-colors duration-150 bg-destructive-100 border border-transparent rounded-full shadow-sm focus:outline-none focus:shadow-2xl active:bg-destructive-200 hover:bg-destructive-200 min-w-28;
}
.btn-upload {
@apply flex min-h-14 w-full items-center justify-center gap-1.5 rounded-md;
}
.btn-primary {
@apply w-fit !bg-primary-200 !text-dark-100 hover:!bg-primary-200/80 !rounded-full !font-bold px-5 cursor-pointer min-h-10;
}
.btn-secondary {
@apply w-fit !bg-dark-200 !text-primary-200 hover:!bg-dark-200/80 !rounded-full !font-bold px-5 cursor-pointer min-h-10;
}
.btn-upload {
@apply bg-dark-200 rounded-full min-h-12 px-5 cursor-pointer border border-input overflow-hidden;
}
.card-border {
@apply border-gradient p-0.5 rounded-2xl w-fit;
}
.card {
@apply dark-gradient rounded-2xl min-h-full;
}
.form {
@apply w-full;
.label {
@apply !text-light-100 !font-normal;
}
.input {
@apply !bg-dark-200 !rounded-full !min-h-12 !px-5 placeholder:!text-light-100;
}
.btn {
@apply !w-full !bg-primary-200 !text-dark-100 hover:!bg-primary-200/80 !rounded-full !min-h-10 !font-bold !px-5 cursor-pointer;
}
}
.call-view {
@apply flex sm:flex-row flex-col gap-10 items-center justify-between w-full;
h3 {
@apply text-center text-primary-100 mt-5;
}
.card-interviewer {
@apply flex-center flex-col gap-2 p-7 h-[400px] blue-gradient-dark rounded-lg border-2 border-primary-200/50 flex-1 sm:basis-1/2 w-full;
}
.avatar {
@apply z-10 flex items-center justify-center blue-gradient rounded-full size-[120px] relative;
.animate-speak {
@apply absolute inline-flex size-5/6 animate-ping rounded-full bg-primary-200 opacity-75;
}
}
.card-border {
@apply border-gradient p-0.5 rounded-2xl flex-1 sm:basis-1/2 w-full h-[400px] max-md:hidden;
}
.card-content {
@apply flex flex-col gap-2 justify-center items-center p-7 dark-gradient rounded-2xl min-h-full;
}
}
.transcript-border {
@apply border-gradient p-0.5 rounded-2xl w-full;
.transcript {
@apply dark-gradient rounded-2xl min-h-12 px-5 py-3 flex items-center justify-center;
p {
@apply text-lg text-center text-white;
}
}
}
.section-feedback {
@apply flex flex-col gap-8 max-w-5xl mx-auto max-sm:px-4 text-lg leading-7;
.buttons {
@apply flex w-full justify-evenly gap-4 max-sm:flex-col max-sm:items-center;
}
}
.auth-layout {
@apply flex items-center justify-center mx-auto max-w-7xl min-h-screen max-sm:px-4 max-sm:py-8;
}
.root-layout {
@apply flex mx-auto max-w-7xl flex-col gap-12 my-12 px-16 max-sm:px-4 max-sm:my-8;
}
.card-cta {
@apply flex flex-row blue-gradient-dark rounded-3xl px-16 py-6 items-center justify-between max-sm:px-4;
}
.interviews-section {
@apply flex flex-wrap gap-4 max-lg:flex-col w-full items-stretch;
}
.interview-text {
@apply text-lg text-center text-white;
}
.progress {
@apply h-1.5 text-[5px] font-bold bg-primary-200 rounded-full flex-center;
}
.tech-tooltip {
@apply absolute bottom-full mb-1 hidden group-hover:flex px-2 py-1 text-xs text-white bg-gray-700 rounded-md shadow-md;
}
.card-interview {
@apply dark-gradient rounded-2xl min-h-full flex flex-col p-6 relative overflow-hidden gap-10 justify-between;
.badge-text {
@apply text-sm font-semibold capitalize;
}
}
}
@utility dark-gradient {
@apply bg-gradient-to-b from-[#1A1C20] to-[#08090D];
}
@utility border-gradient {
@apply bg-gradient-to-b from-[#4B4D4F] to-[#4B4D4F33];
}
@utility pattern {
@apply bg-[url('/pattern.png')] bg-top bg-no-repeat;
}
@utility blue-gradient-dark {
@apply bg-gradient-to-b from-[#171532] to-[#08090D];
}
@utility blue-gradient {
@apply bg-gradient-to-l from-[#FFFFFF] to-[#CAC5FE];
}
@utility flex-center {
@apply flex items-center justify-center;
}
@utility animate-fadeIn {
animation: fadeIn 0.3s ease-in-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(5px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
```
lib/utils.ts
```javascript
import { interviewCovers, mappings } from "@/constants";
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
const techIconBaseURL = "https://cdn.jsdelivr.net/gh/devicons/devicon/icons";
const normalizeTechName = (tech: string) => {
const key = tech.toLowerCase().replace(/\.js$/, "").replace(/\s+/g, "");
return mappings[key as keyof typeof mappings];
};
const checkIconExists = async (url: string) => {
try {
const response = await fetch(url, { method: "HEAD" });
return response.ok; // Returns true if the icon exists
} catch {
return false;
}
};
export const getTechLogos = async (techArray: string[]) => {
const logoURLs = techArray.map((tech) => {
const normalized = normalizeTechName(tech);
return {
tech,
url: `${techIconBaseURL}/${normalized}/${normalized}-original.svg`,
};
});
const results = await Promise.all(
logoURLs.map(async ({ tech, url }) => ({
tech,
url: (await checkIconExists(url)) ? url : "/tech.svg",
}))
);
return results;
};
export const getRandomInterviewCover = () => {
const randomIndex = Math.floor(Math.random() * interviewCovers.length);
return `/covers${interviewCovers[randomIndex]}`;
};
```
Generate questions prompt (/app/api/vapi/generate/route.tsx):
```javascript
`Prepare questions for a job interview.
The job role is ${role}.
The job experience level is ${level}.
The tech stack used in the job is: ${techstack}.
The focus between behavioural and technical questions should lean towards: ${type}.
The amount of questions required is: ${amount}.
Please return only the questions, without any additional text.
The questions are going to be read by a voice assistant so do not use "/" or "*" or any other special characters which might break the voice assistant.
Return the questions formatted like this:
["Question 1", "Question 2", "Question 3"]
Thank you! <3
`;
```
Generate feedback prompt (lib/actions/general.action.ts):
```javascript
prompt: `
You are an AI interviewer analyzing a mock interview. Your task is to evaluate the candidate based on structured categories. Be thorough and detailed in your analysis. Don't be lenient with the candidate. If there are mistakes or areas for improvement, point them out.
Transcript:
${formattedTranscript}
Please score the candidate from 0 to 100 in the following areas. Do not add categories other than the ones provided:
- **Communication Skills**: Clarity, articulation, structured responses.
- **Technical Knowledge**: Understanding of key concepts for the role.
- **Problem-Solving**: Ability to analyze problems and propose solutions.
- **Cultural & Role Fit**: Alignment with company values and job role.
- **Confidence & Clarity**: Confidence in responses, engagement, and clarity.
`,
system:
"You are a professional interviewer analyzing a mock interview. Your task is to evaluate the candidate based on structured categories",
```
Display feedback (app/(root)/interview/[id]/feedback/page.tsx):
```javascript
Feedback on the Interview -{" "}
{interview.role} Interview
Overall Impression:{" "}
{feedback?.totalScore}
/100
{feedback?.createdAt
? dayjs(feedback.createdAt).format("MMM D, YYYY h:mm A")
: "N/A"}
{feedback?.finalAssessment}
Breakdown of the Interview:
{feedback?.categoryScores?.map((category, index) => (
{index + 1}. {category.name} ({category.score}/100)
{category.comment}
))}
Strengths
{feedback?.strengths?.map((strength, index) => (
- {strength}
))}
Areas for Improvement
{feedback?.areasForImprovement?.map((area, index) => (
- {area}
))}
Back to dashboard
Retake Interview
```
Dummy Interviews:
```javascript
export const dummyInterviews: Interview[] = [
{
id: "1",
userId: "user1",
role: "Frontend Developer",
type: "Technical",
techstack: ["React", "TypeScript", "Next.js", "Tailwind CSS"],
level: "Junior",
questions: ["What is React?"],
finalized: false,
createdAt: "2024-03-15T10:00:00Z",
},
{
id: "2",
userId: "user1",
role: "Full Stack Developer",
type: "Mixed",
techstack: ["Node.js", "Express", "MongoDB", "React"],
level: "Senior",
questions: ["What is Node.js?"],
finalized: false,
createdAt: "2024-03-14T15:30:00Z",
},
];
```
## π Demo
Check out the demo of this website [here](https://prepview-alpha.vercel.app/)
I'm a passionate Full Stack Developer who loves turning ideas into digital solutions. I enjoy thinking creatively and tackling challenges. My strong problem-solving skills help me analyze complex issues and find solutions. I believe in constant self-improvement, and giving up is never an option for me.
## π Support
Buy Me a Coffee [here](https://www.buymeacoffee.com/safdarali28)