├── .gitignore
├── .txt
├── LICENSE
├── README.md
├── app
├── (auth)
│ ├── layout.tsx
│ ├── sign-in
│ │ └── page.tsx
│ └── sign-up
│ │ └── page.tsx
├── (dash)
│ ├── dashboard
│ │ └── page.tsx
│ ├── interview
│ │ ├── [id]
│ │ │ ├── feedback
│ │ │ │ └── page.tsx
│ │ │ └── page.tsx
│ │ └── page.tsx
│ └── layout.tsx
├── (root)
│ ├── layout.tsx
│ └── page.tsx
├── api
│ └── google-auth
│ │ ├── route.ts
│ │ └── vapi
│ │ └── generate
│ │ └── route.ts
├── favicon.ico
├── globals.css
└── layout.tsx
├── components.json
├── components
├── Agent.tsx
├── AuthForm.tsx
├── BackToDashboardButton.tsx
├── CheckFeedbackInterviewButton.tsx
├── DisplayTechicons.tsx
├── Features.tsx
├── Footer.tsx
├── FormField.tsx
├── GetInterview.tsx
├── GetStartedbtn.tsx
├── Github.tsx
├── HomeLoginbtn.tsx
├── HomePlay.tsx
├── InterviewCard.tsx
├── Loader.tsx
├── Loading.tsx
├── RetakeInterviewButton.tsx
├── SignOutButton.tsx
├── StartInterviewButton.tsx
├── Testimonials.tsx
└── ui
│ ├── InfiniteCards.tsx
│ ├── button.tsx
│ ├── form.tsx
│ ├── input.tsx
│ ├── label.tsx
│ └── sonner.tsx
├── constants
├── feature.ts
├── index.ts
└── testimonials.ts
├── eslint.config.mjs
├── firebass
├── admin.ts
└── client.ts
├── lib
├── actions
│ ├── auth.action.ts
│ └── generate.action.ts
├── utils.ts
└── vapi.sdk.ts
├── next.config.ts
├── package-lock.json
├── package.json
├── postcss.config.mjs
├── public
├── Chloe RT600.webp
├── Connor.webp
├── ai-avatar.png
├── calendar.svg
├── chloe.webp
├── covers
│ ├── accenture.png
│ ├── adobe.png
│ ├── amazon.png
│ ├── facebook.png
│ ├── flipkart.png
│ ├── google.png
│ ├── hcl.jpg
│ ├── infosys.jpg
│ ├── microsoft.png
│ ├── pinterest.png
│ ├── quora.png
│ ├── reddit.png
│ ├── skype.png
│ ├── spotify.png
│ ├── tcs.png
│ ├── telegram.png
│ └── wipro.png
├── file.svg
├── githubnav.png
├── globe.svg
├── google-icon.png
├── hero.png.webp
├── kara.webp
├── logo.svg
├── logout.png
├── pattern.png
├── profile.svg
├── profile1.webp
├── profile2.webp
├── profile3.webp
├── profile4.webp
├── profile5.webp
├── profile6.webp
├── react.svg
├── star.svg
├── tailwind.svg
├── tech.svg
├── upload.svg
├── user-avatar.png
└── window.svg
├── tsconfig.json
└── types
├── index.d.ts
└── vapi.d.ts
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.*
7 | .yarn/*
8 | !.yarn/patches
9 | !.yarn/plugins
10 | !.yarn/releases
11 | !.yarn/versions
12 |
13 | # testing
14 | /coverage
15 |
16 | # next.js
17 | /.next/
18 | /out/
19 |
20 | # production
21 | /build
22 |
23 | # misc
24 | .DS_Store
25 | *.pem
26 |
27 | # debug
28 | npm-debug.log*
29 | yarn-debug.log*
30 | yarn-error.log*
31 | .pnpm-debug.log*
32 |
33 | # env files (can opt-in for committing if needed)
34 | .env*
35 |
36 | # vercel
37 | .vercel
38 |
39 | # typescript
40 | *.tsbuildinfo
41 | next-env.d.ts
42 |
--------------------------------------------------------------------------------
/.txt:
--------------------------------------------------------------------------------
1 | npx create-nextapp@ /.
2 | npx shadcn@latest init
3 | npm run dev
4 | npx shadcn@latest add button input sonner
5 | npm install dayjs
6 | npm install firebase
7 | npm install firebase-admin --save
8 | npm install ai @ai-sdk/google
9 | npm install @vapi-ai/web
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Abhishek Ganvir
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # 🤖 IntervueAI - Precision in Recruitment
3 | 
4 |
5 |
6 |
7 |
8 | ## 🌟 Overview
9 | The **IntervueAI** is a next-gen AI-powered mock interview platform built to help students and professionals prepare for interviews in a way that actually feels real. It goes beyond generic questions using your resume, job role, and round type to generate personalized, industry-relevant interviews.
10 |
11 | What makes IntervueAI special is its ability to give smart, AI-generated feedback after each session including performance insights, improvement tips, and evaluation reports so you’re not just practicing, you’re leveling up.
12 |
13 | ## ✨ Features
14 | - 🔐 **User Authentication**
15 | - Sign Up and Sign In using password/email authentication handled by Firebase.
16 |
17 | - 🎛️ **Interactive Dashboard**
18 | - Provides an intuitive interface for managing interview preparations.
19 |
20 | - 🧑💻 **No Form-Based Flow**
21 | - Say goodbye to static input forms just log in, speak, and start your interview. It's natural, voice-first interaction from the start.
22 |
23 | - 🤖 **AI-Powered Interview Generation**
24 | - Personalized mock interviews based on your resume, job role, and round type using Vapi voice assistants and Google Gemini.
25 |
26 |
27 | - 🧠 **Digital AI Interviewers**
28 | - Interact with intelligent AI personas inspired by iconic digital characters, making practice more immersive and less robotic.
29 |
30 | - 📝 **Real-Time Transcription**
31 | - Get instant AI-generated feedback after each session, including ratings, improvement tips, and evaluation summaries.
32 |
33 |
34 |
35 | ## 🛠️ Tech Stack
36 | - ⚛ **Next.js**
37 | - 🔥 **Firebase**
38 | - 🎨 **Tailwind CSS**
39 | - 🗣️ **Vapi AI**
40 | - 🧩 **shadcn/ui**
41 | - 🧠 **Google Gemini**
42 |
43 |
44 | ## 🤸 Quick Start
45 |
46 | Follow these steps to set up the project locally on your machine.
47 |
48 |
49 | **Installation**
50 |
51 | Install the project dependencies using npm:
52 |
53 | ```bash
54 | npm install
55 | ```
56 |
57 | **Set Up Environment Variables**
58 |
59 | Create a new file named `.env.local` in the root of your project and add the following content:
60 |
61 | ```env
62 | NEXT_PUBLIC_VAPI_WEB_TOKEN=
63 | NEXT_PUBLIC_VAPI_WORKFLOW_ID=
64 |
65 | GOOGLE_GENERATIVE_AI_API_KEY=
66 |
67 | NEXT_PUBLIC_BASE_URL=
68 |
69 | NEXT_PUBLIC_FIREBASE_API_KEY=
70 | NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=
71 | NEXT_PUBLIC_FIREBASE_PROJECT_ID=
72 | NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=
73 | NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=
74 | NEXT_PUBLIC_FIREBASE_APP_ID=
75 |
76 | FIREBASE_PROJECT_ID=
77 | FIREBASE_CLIENT_EMAIL=
78 | FIREBASE_PRIVATE_KEY=
79 | ```
80 |
81 | Replace the placeholder values with your actual **[Firebase](https://firebase.google.com/)**, **[Vapi](https://vapi.ai/?utm_source=youtube&utm_medium=video&utm_campaign=jsmastery_recruitingpractice&utm_content=paid_partner&utm_term=recruitingpractice)** credentials.
82 |
83 | **Running the Project**
84 |
85 | ```bash
86 | npm run dev
87 | ```
88 |
89 | Open [http://localhost:3000](http://localhost:3000) in your browser to view the project.
90 |
91 | ## 🤝 Contributions
92 | This project is **open-source forever!** Contributions are welcome. Feel free to:
93 | - 🎨 Improve UI/UX
94 | - 🧠 Optimize AI algorithms
95 | - 🗄️ Enhance database efficiency
96 | - 🚀 Add new features
97 |
98 | Fork the repository, make changes, and submit a **pull request**!
99 |
100 | ## 📜 License
101 | This project is licensed under the **MIT License**.
102 |
103 | ---
104 |
105 | ### **🎉 Happy Coding & Best of Luck for Your Interviews! 🚀**
106 |
--------------------------------------------------------------------------------
/app/(auth)/layout.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from 'react'
2 |
3 | import { isAuthenticated } from '@/lib/actions/auth.action'
4 | import { redirect } from 'next/navigation';
5 | const Authlayout = async ({children} : {children: ReactNode}) => {
6 | const isUserAuthenticated = await isAuthenticated();
7 | if (isUserAuthenticated) redirect('/dashboard');
8 | return (
9 |
{children}
10 | )
11 | }
12 | export default Authlayout
--------------------------------------------------------------------------------
/app/(auth)/sign-in/page.tsx:
--------------------------------------------------------------------------------
1 | import AuthForm from '@/components/AuthForm'
2 | import React from 'react'
3 |
4 | const page = () => {
5 | return
6 | }
7 |
8 | export default page
--------------------------------------------------------------------------------
/app/(auth)/sign-up/page.tsx:
--------------------------------------------------------------------------------
1 | import AuthForm from '@/components/AuthForm'
2 | import React from 'react'
3 |
4 | const page = () => {
5 | return
6 | }
7 |
8 | export default page
--------------------------------------------------------------------------------
/app/(dash)/dashboard/page.tsx:
--------------------------------------------------------------------------------
1 |
2 | import InterviewCard from "@/components/InterviewCard"
3 | import StartInterviewButton from "@/components/StartInterviewButton"
4 | import { Button } from "@/components/ui/button"
5 | import {getCurrentUser} from "@/lib/actions/auth.action"
6 | import { getInterviewByUserId, getLatestInterviews } from "@/lib/actions/generate.action"
7 | import Image from "next/image"
8 |
9 |
10 | const page = async () => {
11 | const user = await getCurrentUser();
12 | const [ userInterviews,latestInterviews ] = await Promise.all([
13 | await getInterviewByUserId(user?.id!),
14 | await getLatestInterviews({userId: user?.id! })
15 | ])
16 |
17 | const hasPastInterviews = userInterviews?.length > 0;
18 | const hasUpcomingInterviews = latestInterviews?.length > 0;
19 |
20 | return (
21 | <>
22 |
23 |
24 |
25 |
Get Interview-Ready with Smart Practice and Real Feedback
26 |
27 |
Because every word counts when the job’s on the line
28 |
29 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | Your Interviews
38 |
39 |
{
40 | hasPastInterviews ? (
41 | userInterviews?.map((interview) => (
42 |
43 |
44 | ))) : (
You haven’t taken any Interviews yet
) }
45 | {/**/}
46 |
47 |
48 |
49 |
50 | Take an Interview
51 |
52 |
53 | {
54 | hasUpcomingInterviews ? (
55 | latestInterviews?.map((interview) => (
56 |
57 |
58 | ))) : (
There are no interviews available
) }
59 | {/**/}
60 |
61 |
62 |
63 | >
64 | )
65 | }
66 |
67 | export default page
--------------------------------------------------------------------------------
/app/(dash)/interview/[id]/feedback/page.tsx:
--------------------------------------------------------------------------------
1 | import dayjs from "dayjs";
2 | import Link from "next/link";
3 | import Image from "next/image";
4 | import { redirect } from "next/navigation";
5 |
6 | import {
7 | getFeedbackByInterviewId,
8 | getInterviewById,
9 | } from "@/lib/actions/generate.action"
10 | import { Button } from "@/components/ui/button";
11 | import { getCurrentUser } from "@/lib/actions/auth.action";
12 | import BackToDashboardButton from "@/components/BackToDashboardButton";
13 | import RetakeInterviewButton from "@/components/RetakeInterviewButton";
14 |
15 | const Feedback = async ({ params }: RouteParams) => {
16 | const { id } = await params;
17 | const user = await getCurrentUser();
18 |
19 | const interview = await getInterviewById(id);
20 | if (!interview) redirect("/");
21 |
22 | const feedback = await getFeedbackByInterviewId({
23 | interviewId: id,
24 | userId: user?.id!,
25 | });
26 |
27 | return (
28 |
29 |
30 |
31 | Feedback on the Interview -{" "}
32 | {interview.role} Interview
33 |
34 |
35 |
36 |
37 |
38 | {/* Overall Impression */}
39 |
40 |
41 |
42 | Overall Impression:{" "}
43 |
44 | {feedback?.totalScore}
45 |
46 | /100
47 |
48 |
49 |
50 | {/* Date */}
51 |
52 |
53 |
54 | {feedback?.createdAt
55 | ? dayjs(feedback.createdAt).format("MMM D, YYYY h:mm A")
56 | : "N/A"}
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 | {feedback?.finalAssessment}
65 |
66 | {/* Interview Breakdown */}
67 |
68 |
Breakdown of the Interview:
69 | {feedback?.categoryScores?.map((category, index) => (
70 |
71 |
72 | {index + 1}. {category.name} ({category.score}/100)
73 |
74 |
{category.comment}
75 |
76 | ))}
77 |
78 |
79 |
80 |
Strengths
81 |
82 | {feedback?.strengths?.map((strength, index) => (
83 | - {strength}
84 | ))}
85 |
86 |
87 |
88 |
89 |
Areas for Improvement
90 |
91 | {feedback?.areasForImprovement?.map((area, index) => (
92 | - {area}
93 | ))}
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 | );
103 | };
104 |
105 | export default Feedback;
--------------------------------------------------------------------------------
/app/(dash)/interview/[id]/page.tsx:
--------------------------------------------------------------------------------
1 |
2 | import GetInterview from "@/components/GetInterview"
3 | import DisplayTechicons from "@/components/DisplayTechicons";
4 | import { getCurrentUser } from "@/lib/actions/auth.action";
5 | import { getInterviewById } from "@/lib/actions/generate.action";
6 | import { getRandomInterviewCover } from "@/lib/utils";
7 | import Image from "next/image";
8 | import { redirect } from "next/navigation";
9 |
10 | const page = async ({ params }: RouteParams) => {
11 | const {id} = await params;
12 | const user = await getCurrentUser();
13 | const interview = await getInterviewById(id);
14 | if(!interview) redirect('/dashboard')
15 | return (
16 | <>
17 |
18 |
19 |
20 |
21 |
{interview.role}
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
42 | >
43 | )
44 | }
45 |
46 | export default page
--------------------------------------------------------------------------------
/app/(dash)/interview/page.tsx:
--------------------------------------------------------------------------------
1 | import Agent from '@/components/Agent'
2 | import { getCurrentUser } from '@/lib/actions/auth.action'
3 | import React from 'react'
4 |
5 | const page = async () => {
6 | const user = await getCurrentUser();
7 | return (
8 | <>
9 | Interview Generation
10 |
11 | >
12 | )
13 | }
14 |
15 | export default page
--------------------------------------------------------------------------------
/app/(dash)/layout.tsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image'
2 | import Link from 'next/link'
3 | import React, { ReactNode } from 'react'
4 | import { isAuthenticated } from '@/lib/actions/auth.action'
5 | import { redirect } from 'next/navigation';
6 | import SignOutButton from '@/components/SignOutButton';
7 | const Rootlayout = async ({children} : {children: ReactNode}) => {
8 | const isUserAuthenticated = await isAuthenticated();
9 | if (!isUserAuthenticated) redirect('/');
10 | return (
11 |
12 |
19 | {children}
20 |
21 | )
22 | }
23 |
24 | export default Rootlayout
--------------------------------------------------------------------------------
/app/(root)/layout.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from 'react'
2 | import { isAuthenticated } from '@/lib/actions/auth.action'
3 | import { redirect } from 'next/navigation';
4 | import Link from 'next/link';
5 | import Image from 'next/image';
6 | import HomeLoginbtn from '@/components/HomeLoginbtn';
7 | const Rootlayout = async ({children} : {children: ReactNode}) => {
8 | const isUserAuthenticated = await isAuthenticated();
9 | if (isUserAuthenticated) redirect('/dashboard');
10 | return (
11 |
12 |
21 | {children}
22 |
23 | )
24 | }
25 |
26 | export default Rootlayout
--------------------------------------------------------------------------------
/app/(root)/page.tsx:
--------------------------------------------------------------------------------
1 | // app/page.tsx
2 | "use client"
3 | import Features from '@/components/Features';
4 | import Footer from '@/components/Footer';
5 | import GetStartedbtn from '@/components/GetStartedbtn';
6 | import HomePlay from '@/components/HomePlay';
7 | import Testimonials from '@/components/Testimonials';
8 |
9 | //import { isAuthenticated } from '@/lib/actions/auth.action'
10 | import Link from 'next/link'
11 | //import { redirect } from 'next/navigation'
12 | import React, { useEffect, useState } from 'react';
13 |
14 | export default function LandingPage() {
15 | const [mounted, setMounted] = useState(false);
16 |
17 | useEffect(() => {
18 | setMounted(true);
19 | }, []);
20 |
21 | if (!mounted) return null;
22 | return (
23 | <>
24 | <>
25 |
26 |
27 |
Where everyone
28 |
29 | suffers together
30 |
31 |
We know how brutal interviews can be. They don’t have to be.
32 |
Generate personalized mock interviews, watch how others handled theirs,
33 |
and get feedback that actually helps.
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | < GetStartedbtn />
42 |
43 |
44 |
45 |
46 |
47 | < HomePlay />
48 |
49 |
50 |
51 | {/* This wrapper shouldn't limit full-screen sections */}
52 |
53 |
54 | {/* Normal content (centered) */}
55 |
56 | {/* ...hero text */}
57 | WHY CHOOSE US
58 | Unleash Your Potential with AI
59 | Take your interview preparation to the next level with features designed for success.
60 |
61 |
62 | {/* Full-screen wide section */}
63 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 | >
78 | >
79 | )
80 | }
81 |
--------------------------------------------------------------------------------
/app/api/google-auth/route.ts:
--------------------------------------------------------------------------------
1 | // app/api/google-auth/route.ts
2 | import { NextRequest, NextResponse } from "next/server";
3 | import { db } from "@/firebass/admin";
4 | import { setSessionCookie } from "@/lib/actions/auth.action";
5 |
6 | export async function POST(req: NextRequest) {
7 | try {
8 | const { uid, name, email, idToken } = await req.json();
9 |
10 | // Check if user exists in Firestore
11 | const userRef = db.collection("users").doc(uid);
12 | const userDoc = await userRef.get();
13 |
14 | if (!userDoc.exists) {
15 | // Save to Firestore if new user
16 | await userRef.set({
17 | name,
18 | email,
19 | });
20 | }
21 |
22 | // Set session cookie
23 | await setSessionCookie(idToken);
24 |
25 | return NextResponse.json({ success: true, message: "User signed in successfully." });
26 | } catch (error) {
27 | console.error("Google Auth error:", error);
28 | return NextResponse.json({ success: false, message: "Something went wrong." }, { status: 500 });
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/app/api/google-auth/vapi/generate/route.ts:
--------------------------------------------------------------------------------
1 | import { generateText } from "ai";
2 | import { google } from "@ai-sdk/google";
3 |
4 |
5 | import { getRandomInterviewCover } from "@/lib/utils";
6 | import { db } from "@/firebass/admin";
7 |
8 | export async function GET() {
9 | return Response.json({ success: true, data: 'THANK YOU!'}, {status:200});
10 | }
11 |
12 | export async function POST(request: Request){
13 | const { type, role , level, techstack, amount, profile, userid } = await request.json();
14 |
15 | try {
16 | const { text: questions } = await generateText({
17 | model: google("gemini-2.0-flash-001"),
18 | prompt: `Prepare questions for a job interview.
19 | The job role is ${role}.
20 | The job experience level is ${level}.
21 | The tech stack used in the job is: ${techstack}.
22 | The focus between behavioural and technical questions should lean towards: ${type}.
23 | The user's cv/resume profile is: ${profile}.
24 | The amount of questions required is: ${amount}.
25 | Please return only the questions, without any additional text.
26 | 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.
27 | Return the questions formatted like this:
28 | ["Question 1", "Question 2", "Question 3"]
29 |
30 | Thank you! <3
31 | `,
32 | });
33 | const interview = {
34 | role: role,
35 | type: type,
36 | level: level,
37 | profile: profile,
38 | techstack: techstack.split(","),
39 | questions: JSON.parse(questions),
40 | userId: userid,
41 | finalized: true,
42 | coverImage: getRandomInterviewCover(),
43 | createdAt: new Date().toISOString(),
44 | };
45 |
46 | await db.collection("interviews").add(interview);
47 |
48 | return Response.json({success: true}, {status:200})
49 |
50 | } catch (error) {
51 | console.error(error);
52 | }
53 | }
--------------------------------------------------------------------------------
/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/b8fa3b1c6971998fbecc2f0c104bfb22cf12257c/app/favicon.ico
--------------------------------------------------------------------------------
/app/globals.css:
--------------------------------------------------------------------------------
1 | @import "tailwindcss";
2 |
3 | @plugin "tailwindcss-animate";
4 |
5 | @custom-variant dark (&:is(.dark *));
6 |
7 | @theme {
8 | --color-success-100: #49de50;
9 | --color-success-200: #42c748;
10 | --color-destructive-100: #f75353;
11 | --color-destructive-200: #c44141;
12 |
13 | --color-primary-100: #dddfff;
14 | --color-primary-200: #cac5fe;
15 |
16 | --color-light-100: #d6e0ff;
17 | --color-light-400: #6870a6;
18 | --color-light-600: #4f557d;
19 | --color-light-800: #24273a;
20 |
21 | --color-dark-100: #020408;
22 | --color-dark-200: #27282f;
23 | --color-dark-300: #242633;
24 |
25 | --font-mona-sans: "Mona Sans", sans-serif;
26 |
27 | --bg-pattern: url("/pattern.png");
28 | }
29 |
30 | :root {
31 | --radius: 0.625rem;
32 | --background: oklch(1 0 0);
33 | --foreground: oklch(0.145 0 0);
34 | --card: oklch(1 0 0);
35 | --card-foreground: oklch(0.145 0 0);
36 | --popover: oklch(1 0 0);
37 | --popover-foreground: oklch(0.145 0 0);
38 | --primary: oklch(0.205 0 0);
39 | --primary-foreground: oklch(0.985 0 0);
40 | --secondary: oklch(0.97 0 0);
41 | --secondary-foreground: oklch(0.205 0 0);
42 | --muted: oklch(0.97 0 0);
43 | --muted-foreground: oklch(0.556 0 0);
44 | --accent: oklch(0.97 0 0);
45 | --accent-foreground: oklch(0.205 0 0);
46 | --destructive: oklch(0.577 0.245 27.325);
47 | --border: oklch(0.922 0 0);
48 | --input: oklch(0.922 0 0);
49 | --ring: oklch(0.708 0 0);
50 | --chart-1: oklch(0.646 0.222 41.116);
51 | --chart-2: oklch(0.6 0.118 184.704);
52 | --chart-3: oklch(0.398 0.07 227.392);
53 | --chart-4: oklch(0.828 0.189 84.429);
54 | --chart-5: oklch(0.769 0.188 70.08);
55 | --sidebar: oklch(0.985 0 0);
56 | --sidebar-foreground: oklch(0.145 0 0);
57 | --sidebar-primary: oklch(0.205 0 0);
58 | --sidebar-primary-foreground: oklch(0.985 0 0);
59 | --sidebar-accent: oklch(0.97 0 0);
60 | --sidebar-accent-foreground: oklch(0.205 0 0);
61 | --sidebar-border: oklch(0.922 0 0);
62 | --sidebar-ring: oklch(0.708 0 0);
63 | }
64 |
65 | .dark {
66 | --background: oklch(0.145 0 0);
67 | --foreground: oklch(0.985 0 0);
68 | --card: oklch(0.205 0 0);
69 | --card-foreground: oklch(0.985 0 0);
70 | --popover: oklch(0.205 0 0);
71 | --popover-foreground: oklch(0.985 0 0);
72 | --primary: oklch(0.922 0 0);
73 | --primary-foreground: oklch(0.205 0 0);
74 | --secondary: oklch(0.269 0 0);
75 | --secondary-foreground: oklch(0.985 0 0);
76 | --muted: oklch(0.269 0 0);
77 | --muted-foreground: var(--light-100);
78 | --accent: oklch(0.269 0 0);
79 | --accent-foreground: oklch(0.985 0 0);
80 | --destructive: oklch(0.704 0.191 22.216);
81 | --border: oklch(1 0 0 / 10%);
82 | --input: oklch(1 0 0 / 15%);
83 | --ring: oklch(0.556 0 0);
84 | --chart-1: oklch(0.488 0.243 264.376);
85 | --chart-2: oklch(0.696 0.17 162.48);
86 | --chart-3: oklch(0.769 0.188 70.08);
87 | --chart-4: oklch(0.627 0.265 303.9);
88 | --chart-5: oklch(0.645 0.246 16.439);
89 | --sidebar: oklch(0.205 0 0);
90 | --sidebar-foreground: oklch(0.985 0 0);
91 | --sidebar-primary: oklch(0.488 0.243 264.376);
92 | --sidebar-primary-foreground: oklch(0.985 0 0);
93 | --sidebar-accent: oklch(0.269 0 0);
94 | --sidebar-accent-foreground: oklch(0.985 0 0);
95 | --sidebar-border: oklch(1 0 0 / 10%);
96 | --sidebar-ring: oklch(0.556 0 0);
97 | }
98 |
99 | @theme inline {
100 | --radius-sm: calc(var(--radius) - 4px);
101 | --radius-md: calc(var(--radius) - 2px);
102 | --radius-lg: var(--radius);
103 | --radius-xl: calc(var(--radius) + 4px);
104 | --color-background: var(--background);
105 | --color-foreground: var(--foreground);
106 | --color-card: var(--card);
107 | --color-card-foreground: var(--card-foreground);
108 | --color-popover: var(--popover);
109 | --color-popover-foreground: var(--popover-foreground);
110 | --color-primary: var(--primary);
111 | --color-primary-foreground: var(--primary-foreground);
112 | --color-secondary: var(--secondary);
113 | --color-secondary-foreground: var(--secondary-foreground);
114 | --color-muted: var(--muted);
115 | --color-muted-foreground: var(--muted-foreground);
116 | --color-accent: var(--accent);
117 | --color-accent-foreground: var(--accent-foreground);
118 | --color-destructive: var(--destructive);
119 | --color-border: var(--border);
120 | --color-input: var(--input);
121 | --color-ring: var(--ring);
122 | --color-chart-1: var(--chart-1);
123 | --color-chart-2: var(--chart-2);
124 | --color-chart-3: var(--chart-3);
125 | --color-chart-4: var(--chart-4);
126 | --color-chart-5: var(--chart-5);
127 | --color-sidebar: var(--sidebar);
128 | --color-sidebar-foreground: var(--sidebar-foreground);
129 | --color-sidebar-primary: var(--sidebar-primary);
130 | --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
131 | --color-sidebar-accent: var(--sidebar-accent);
132 | --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
133 | --color-sidebar-border: var(--sidebar-border);
134 | --color-sidebar-ring: var(--sidebar-ring);
135 | }
136 |
137 | @layer base {
138 | * {
139 | @apply border-border outline-ring/50;
140 | }
141 | body {
142 | @apply bg-background text-foreground;
143 | }
144 | p {
145 | @apply text-light-100;
146 | }
147 | h2 {
148 | @apply text-3xl font-semibold;
149 | }
150 | h3 {
151 | @apply text-2xl font-semibold;
152 | }
153 | ul {
154 | @apply list-disc list-inside;
155 | }
156 | li {
157 | @apply text-light-100;
158 | }
159 | }
160 |
161 | @layer components {
162 | .btn-call {
163 | @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;
164 |
165 | .span {
166 | @apply bg-success-100 h-[85%] w-[65%];
167 | }
168 | }
169 |
170 | .btn-disconnect {
171 | @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;
172 | }
173 |
174 | .btn-upload {
175 | @apply flex min-h-14 w-full items-center justify-center gap-1.5 rounded-md;
176 | }
177 | .btn-primary {
178 | @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;
179 | }
180 | .btn-secondary {
181 | @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;
182 | }
183 |
184 | .btn-upload {
185 | @apply bg-dark-200 rounded-full min-h-12 px-5 cursor-pointer border border-input overflow-hidden;
186 | }
187 |
188 | .card-border {
189 | @apply border-gradient p-0.5 rounded-2xl w-fit;
190 | }
191 |
192 | .card {
193 | @apply dark-gradient rounded-2xl min-h-full;
194 | }
195 |
196 | .form {
197 | @apply w-full;
198 |
199 | .label {
200 | @apply !text-light-100 !font-normal;
201 | }
202 |
203 | .input {
204 | @apply !bg-dark-200 !rounded-full !min-h-12 !px-5 placeholder:!text-light-100;
205 | }
206 |
207 | .btn {
208 | @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;
209 | }
210 | }
211 |
212 | .call-view {
213 | @apply flex sm:flex-row flex-col gap-10 items-center justify-between w-full;
214 |
215 | h3 {
216 | @apply text-center text-primary-100 mt-5;
217 | }
218 |
219 | .card-interviewer {
220 | @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;
221 | }
222 |
223 | .avatar {
224 | @apply z-10 flex items-center justify-center blue-gradient rounded-full size-[120px] relative;
225 |
226 | .animate-speak {
227 | @apply absolute inline-flex size-5/6 animate-ping rounded-full bg-primary-200 opacity-75;
228 | }
229 | }
230 |
231 | .card-border {
232 | @apply border-gradient p-0.5 rounded-2xl flex-1 sm:basis-1/2 w-full h-[400px] max-md:hidden;
233 | }
234 |
235 | .card-content {
236 | @apply flex flex-col gap-2 justify-center items-center p-7 dark-gradient rounded-2xl min-h-full;
237 | }
238 | }
239 |
240 | .transcript-border {
241 | @apply border-gradient p-0.5 rounded-2xl w-full;
242 |
243 | .transcript {
244 | @apply dark-gradient rounded-2xl min-h-12 px-5 py-3 flex items-center justify-center;
245 |
246 | p {
247 | @apply text-lg text-center text-white;
248 | }
249 | }
250 | }
251 |
252 | .section-feedback {
253 | @apply flex flex-col gap-8 max-w-5xl mx-auto max-sm:px-4 text-lg leading-7;
254 |
255 | .buttons {
256 | @apply flex w-full justify-evenly gap-4 max-sm:flex-col max-sm:items-center;
257 | }
258 | }
259 |
260 | .auth-layout {
261 | @apply flex items-center justify-center mx-auto max-w-7xl min-h-screen max-sm:px-4 max-sm:py-8;
262 | }
263 |
264 | .root-layout {
265 | @apply flex mx-auto max-w-7xl flex-col gap-12 my-12 px-16 max-sm:px-4 max-sm:my-8;
266 | }
267 |
268 | .card-cta {
269 | @apply flex flex-row blue-gradient-dark rounded-3xl px-16 py-6 items-center justify-between max-sm:px-4;
270 | }
271 |
272 | .interviews-section {
273 | @apply flex flex-wrap gap-4 max-lg:flex-col w-full items-stretch;
274 | }
275 |
276 | .interview-text {
277 | @apply text-lg text-center text-white;
278 | }
279 |
280 | .progress {
281 | @apply h-1.5 text-[5px] font-bold bg-primary-200 rounded-full flex-center;
282 | }
283 |
284 | .tech-tooltip {
285 | @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;
286 | }
287 |
288 | .card-interview {
289 | @apply dark-gradient rounded-2xl min-h-full flex flex-col p-6 relative overflow-hidden gap-10 justify-between;
290 |
291 | .badge-text {
292 | @apply text-sm font-semibold capitalize;
293 | }
294 | }
295 | }
296 |
297 | @utility dark-gradient {
298 | @apply bg-gradient-to-b from-[#1A1C20] to-[#08090D];
299 | }
300 |
301 | @utility border-gradient {
302 | @apply bg-gradient-to-b from-[#4B4D4F] to-[#4B4D4F33];
303 | }
304 |
305 | @utility pattern {
306 | @apply bg-[url('/pattern.png')] bg-top bg-no-repeat;
307 | }
308 |
309 | @utility blue-gradient-dark {
310 | @apply bg-gradient-to-b from-[#171532] to-[#08090D];
311 | }
312 |
313 | @utility blue-gradient {
314 | @apply bg-gradient-to-l from-[#FFFFFF] to-[#CAC5FE];
315 | }
316 |
317 | @utility flex-center {
318 | @apply flex items-center justify-center;
319 | }
320 |
321 | @utility animate-fadeIn {
322 | animation: fadeIn 0.3s ease-in-out;
323 | }
324 | /* Hide scrollbar but allow scrolling */
325 | ::-webkit-scrollbar {
326 | display: none;
327 | }
328 |
329 | /* Ensure scrolling still works */
330 | body {
331 | scrollbar-width: none; /* Firefox */
332 | -ms-overflow-style: none; /* Internet Explorer/Edge */
333 | }
334 |
335 | @keyframes fadeIn {
336 | from {
337 | opacity: 0;
338 | transform: translateY(5px);
339 | }
340 | to {
341 | opacity: 1;
342 | transform: translateY(0);
343 | }
344 | }
345 | @media (min-width: 768px) and (max-width: 884px) {
346 | .interview-section {
347 | grid-template-columns: repeat(1, minmax(0, 1fr));
348 | place-items: center;
349 | justify-content: center;
350 |
351 | }
352 | .interview-wrapper {
353 | display: flex;
354 | justify-content: center;
355 | }
356 | }
357 |
358 | @media (min-width: 1027px) and (max-width: 1245px) {
359 | .interview-section {
360 | grid-template-columns: repeat(2, minmax(0, 2fr));
361 | }
362 | .interview-wrapper {
363 | display: flex;
364 | justify-content: center;
365 | }
366 | }
367 | @media (min-width: 1027px) and (max-width: 1245px) {
368 | .homeplaylayout{
369 | margin-bottom: 20%;
370 |
371 | }
372 | }
373 | @media (min-width: 668px) and (max-width: 768px) {
374 | .homeplay-icon{
375 | margin-bottom: -40%;
376 | }
377 | }
378 | @media (min-width: 800px) and (max-width: 900px) {
379 | .homeplay-icon{
380 | margin-bottom: -28%;
381 | }
382 | }
383 | @media (min-width: 1000px) and (max-width: 1100px) {
384 | .homeplay-icon{
385 | margin-bottom: -15%;
386 | }
387 | }
388 | @media (min-width: 902px) and (max-width: 980px) {
389 | .homeplay-icon{
390 | margin-bottom: -20%;
391 | }
392 | }
393 | @media (min-width: 984px) and (max-width: 999px) {
394 | .homeplay-icon{
395 | margin-bottom: -20%;
396 | }
397 | }
398 | @media (min-width: 1105px) and (max-width: 1248px) {
399 | .homeplay-icon{
400 | margin-bottom: -1%;
401 | }
402 | }
403 | .glow-border {
404 | background: radial-gradient(
405 | ellipse at center,
406 | rgba(168, 85, 247, 0.5) 0%,
407 | transparent 40%
408 | );
409 | filter: blur(40px);
410 | z-index: 0;
411 | }
412 | .hero-image-wrapper {
413 | perspective: 1000px;
414 | overflow: hidden;
415 | }
416 |
417 | .hero-image {
418 | transform: rotateX(15deg) scale(1);
419 | transition: transform 0.5s ease-out;
420 | will-change: transform;
421 | }
422 |
423 | .hero-image.scrolled {
424 | transform: rotateX(0deg) scale(1) translateY(40px);
425 | }
426 | @keyframes scrollAnimation {
427 | 0% {
428 | transform: translateX(0);
429 | }
430 | 100% {
431 | transform: translateX(-100%);
432 | }
433 | }
434 |
435 | .animate-scroll {
436 | animation: scrollAnimation var(--animation-duration) linear infinite;
437 | }
438 | @layer utilities {
439 | .heading {
440 | @apply font-bold text-4xl md:text-5xl text-center;
441 | }
442 | }
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import { Mona_Sans } from "next/font/google";
3 | import "./globals.css";
4 | import { Toaster } from "sonner";
5 |
6 | const monaSans = Mona_Sans({
7 | variable: "--font-mona-sans",
8 | subsets: ["latin"],
9 | //display: "swap", // ⬅️ improves font performance
10 | });
11 |
12 |
13 |
14 | export const metadata: Metadata = {
15 | title: "IntervueAI - Precision in Recruitment",
16 | description: "IntervueAI is an AI-driven interview platform with real-time voice integration and automated feedback, helping users prepare faster and smarter.",
17 | };
18 |
19 | export default function RootLayout({
20 | children,
21 | }: Readonly<{
22 | children: React.ReactNode;
23 | }>) {
24 | return (
25 |
26 |
29 | {children}
30 |
31 |
32 |
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "",
8 | "css": "app/globals.css",
9 | "baseColor": "neutral",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | },
20 | "iconLibrary": "lucide"
21 | }
--------------------------------------------------------------------------------
/components/Agent.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { cn } from '@/lib/utils';
4 | import { vapi } from '@/lib/vapi.sdk';
5 |
6 |
7 | import Image from 'next/image'
8 | import { useRouter } from 'next/navigation';
9 | import React, { useEffect, useState } from 'react'
10 | import Loading from './Loading';
11 |
12 | enum CallStatus {
13 | INACTIVE = 'INACTIVE',
14 | CONNECTING = 'CONNECTING',
15 | ACTIVE = 'ACTIVE',
16 | FINISHED = 'FINISHED',
17 | }
18 |
19 | interface SavedMessage {
20 | role: 'user' | 'system' | 'assistant';
21 | content: string;
22 | }
23 | const Agent = ({userName, userId, type}: AgentProps) => {
24 | const router = useRouter();
25 | const [isRedirecting, setIsRedirecting] = useState(false)
26 | const [isSpeaking, setIsSpeaking] = useState(false);
27 | const [callStatus, setCallStatus] = useState(CallStatus.INACTIVE)
28 | const [messages, setMessages] = useState([]);
29 |
30 | // const callStatus = CallStatus.ACTIVE;
31 | //const isSpeaking = true;
32 | //const messages = [
33 | // 'Whats your name?',
34 | // 'My name is John Doe, nice to meet you!'
35 | //];
36 |
37 | useEffect(() => {
38 | const onCallStart = () => setCallStatus(CallStatus.ACTIVE);
39 | const onCallEnd = () => setCallStatus(CallStatus.FINISHED);
40 | const onMessage = (message: Message) => {
41 | if(message.type === 'transcript' && message.transcriptType === 'final'){
42 | const newMessage = { role: message.role, content: message.transcript}
43 | setMessages((prev) => [...prev,newMessage]);
44 | }
45 | }
46 | const onSpeechStart = () => setIsSpeaking(true);
47 | const onSpeechEnd = () => setIsSpeaking(false);
48 | const onError = (error: Error) => console.log('Error',error);
49 | vapi.on('call-start',onCallStart);
50 | vapi.on('call-end',onCallEnd);
51 | vapi.on('message',onMessage)
52 | vapi.on('speech-start',onSpeechStart);
53 | vapi.on('speech-end',onSpeechEnd);
54 | vapi.on('error',onError);
55 |
56 | return () => {
57 | vapi.off('call-start',onCallStart);
58 | vapi.off('call-end',onCallEnd);
59 | vapi.off('message',onMessage)
60 | vapi.off('speech-start',onSpeechStart);
61 | vapi.off('speech-end',onSpeechEnd);
62 | vapi.off('error',onError);
63 | }
64 | }, [])
65 | useEffect(() => {
66 | if(callStatus === CallStatus.FINISHED) {
67 | setIsRedirecting(true); // Trigger loader before navigation
68 | router.push('/dashboard');
69 | }
70 |
71 | },[callStatus, router]); // ✅ include dependencies)
72 |
73 | const handleCall = async () => {
74 | setCallStatus(CallStatus.CONNECTING);
75 | await vapi.start(process.env.NEXT_PUBLIC_VAPI_WORKFLOW_ID!,{
76 | variableValues:{
77 | username: userName,
78 | userid:userId,
79 | }
80 | })
81 | }
82 | const handleDisconnect = async () => {
83 | setCallStatus(CallStatus.FINISHED);
84 | vapi.stop();
85 | }
86 |
87 | const latestMessage = messages[messages.length - 1]?.content;
88 |
89 | const isCallInactiveOrFinished = callStatus === CallStatus.INACTIVE || callStatus === CallStatus.FINISHED;
90 |
91 | return (
92 | <>
93 |
94 | {isRedirecting && (
95 |
96 |
97 |
98 | )}
99 |
100 |
{isSpeaking && }
101 |
Connor
102 |
Interview Architect
103 |
104 |
105 |
106 |
107 |
108 |
{userName}
109 |
110 |
111 |
112 |
113 | {messages.length > 0 && (
114 |
115 |
116 |
{latestMessage}
117 |
118 |
119 | )}
120 |
121 |
122 | {callStatus !== 'ACTIVE' ? (
123 |
131 | ) : (
132 |
135 | )}
136 |
137 |
138 | >
139 | )
140 | }
141 |
142 | export default Agent
--------------------------------------------------------------------------------
/components/AuthForm.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 | import { zodResolver } from "@hookform/resolvers/zod"
3 | import { useForm } from "react-hook-form"
4 | import { z } from "zod"
5 | import { useState } from "react"
6 | import { Button } from "@/components/ui/button"
7 | import {
8 | Form
9 | } from "@/components/ui/form"
10 | import Image from "next/image"
11 | import Link from "next/link"
12 | import { toast } from "sonner"
13 | import FormField from "./FormField"
14 | import { useRouter } from "next/navigation"
15 | import { createUserWithEmailAndPassword, signInWithEmailAndPassword } from "firebase/auth";
16 | import { auth } from "@/firebass/client";
17 | import { signIn, signUp } from "@/lib/actions/auth.action";
18 | import Loading from "./Loading"
19 |
20 |
21 | const AuthFormSchema = (type: FormType) => {return z.object ({
22 | name: type === 'sign-up' ? z.string().min(3) : z.string().optional(),
23 | email: z.string().email(),
24 | password: z.string().min(6),
25 | })}
26 |
27 |
28 | const AuthForm = ({type}: {type: FormType}) => {
29 | const router = useRouter();
30 | const formSchema = AuthFormSchema(type);
31 | // Add loading state
32 | const [isLoading, setIsLoading] = useState(false);
33 |
34 | // 1. Define your form.
35 | const form = useForm>({
36 | resolver: zodResolver(formSchema),
37 | defaultValues: {
38 | name: "",
39 | email: "",
40 | password:"",
41 | },
42 | })
43 |
44 | // 2. Define a submit handler.
45 | async function onSubmit(values: z.infer) {
46 | // Set loading to true when form is submitted
47 | setIsLoading(true);
48 |
49 | try {
50 | if(type === 'sign-up') {
51 | const { name, email, password } = values;
52 | const userCredentials = await createUserWithEmailAndPassword(auth,email,password)
53 | const result = await signUp({
54 | uid: userCredentials.user.uid,
55 | name: name!,
56 | email,
57 | password,
58 | })
59 | if (!result?.success){
60 | toast.error(result?.message);
61 | setIsLoading(false);
62 | return;
63 | }
64 |
65 | toast.success("Account created successfully.")
66 | router.push('/dashboard')
67 | } else {
68 | const {email,password} = values;
69 | const userCredential = await signInWithEmailAndPassword(auth,email,password);
70 | const idToken = await userCredential.user.getIdToken();
71 | if(!idToken) {
72 | toast.error('Sign in failed')
73 | setIsLoading(false);
74 | return;
75 | }
76 | await signIn({ email, idToken})
77 | toast.success("Sign in successfully.");
78 | router.push('/dashboard')
79 | }
80 | } catch(error){
81 | console.log(error);
82 | toast.error(`There is an error: ${error}`);
83 | // Set loading to false on error
84 | setIsLoading(false);
85 | }
86 | }
87 |
88 | const isSignIn = type === 'sign-in';
89 |
90 | return (
91 |
92 | {/* Full screen loader overlay */}
93 | {isLoading && (
94 |
{isSignIn ? : }
95 | )}
96 |
97 |
98 |
99 |
IntervueAI
100 |
101 |
Practice Like You Truly Mean It
102 |
103 |
160 |
161 |
162 | {isSignIn ? 'No account yet?': 'Have an account already?'}
163 |
164 |
165 | {!isSignIn ? "Sign in" : 'Sign up'}
166 |
167 |
168 |
169 | )
170 | }
171 |
172 | export default AuthForm
--------------------------------------------------------------------------------
/components/BackToDashboardButton.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useState } from "react"
4 | import { useRouter } from "next/navigation"
5 | import { Button } from "@/components/ui/button"
6 | import Loading from "@/components/Loading"
7 |
8 | const BackToDashboardButton = () => {
9 | const [loading, setLoading] = useState(false)
10 | const router = useRouter()
11 |
12 | const handleClick = () => {
13 | setLoading(true)
14 | router.push("/dashboard")
15 | }
16 |
17 | return (
18 |
24 | )
25 | }
26 |
27 | export default BackToDashboardButton
28 |
--------------------------------------------------------------------------------
/components/CheckFeedbackInterviewButton.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useState } from "react"
4 | import { useRouter } from "next/navigation"
5 | import { Button } from "./ui/button"
6 | import Loading from "./Loading"
7 |
8 | interface Props {
9 | hasFeedback: boolean
10 | interviewId: string
11 | }
12 |
13 | const CheckFeedbackInterviewButton = ({ hasFeedback, interviewId }: Props) => {
14 | const [loading, setLoading] = useState(false)
15 | const router = useRouter()
16 |
17 | const handleClick = () => {
18 | setLoading(true)
19 | const url = hasFeedback
20 | ? `/interview/${interviewId}/feedback`
21 | : `/interview/${interviewId}`
22 | router.push(url)
23 | }
24 |
25 | return (
26 |
30 | )
31 | }
32 |
33 | export default CheckFeedbackInterviewButton
34 |
--------------------------------------------------------------------------------
/components/DisplayTechicons.tsx:
--------------------------------------------------------------------------------
1 |
2 | import { getTechLogos } from '@/lib/utils';
3 | import Image from 'next/image';
4 | import React from 'react'
5 |
6 | const DisplayTechicons = async ({techStack}: TechIconProps) => {
7 | const techIcons = await getTechLogos(techStack)
8 | return (
9 | {techIcons.slice(0,3).map (({tech, url}, index)=>
10 |
11 |
12 | {tech}
13 |
14 |
15 | )}
16 | )
17 | }
18 |
19 | export default DisplayTechicons;
--------------------------------------------------------------------------------
/components/Features.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import {features} from '@/constants/feature'
4 | const Features = () => {
5 | return (
6 |
7 | {features.map((feature, index) => (
8 |
9 |
10 |
11 |
12 |
{feature.title}
13 |
{feature.description}
14 |
15 |
16 |
17 |
18 |
19 |
20 | ))}
21 |
22 | );
23 | }
24 |
25 | const StyledWrapper = styled.div`
26 | display: flex;
27 | flex-direction: column; /* Stack items vertically */
28 | gap: 2rem; /* spacing between cards */
29 | align-items: center;
30 | @media (min-width: 640px) {
31 | flex-direction: row; /* Row layout on sm and up */
32 | flex-wrap: wrap;
33 | justify-content: center;
34 | }
35 | .outer {
36 | width: 300px;
37 | height: 250px;
38 | border-radius: 10px;
39 | padding: 1px;
40 | background: radial-gradient(circle 230px at 0% 0%, #ffffff, #9F79C1);
41 | position: relative;
42 |
43 |
44 | }
45 |
46 | .dot {
47 | width: 5px;
48 | aspect-ratio: 1;
49 | position: absolute;
50 | background-color: #fff;
51 | box-shadow: 0 0 10px #ffffff;
52 | border-radius: 100px;
53 | z-index: 2;
54 | right: 10%;
55 | top: 10%;
56 | animation: moveDot 6s linear infinite;
57 | }
58 |
59 | @keyframes moveDot {
60 | 0%,
61 | 100% {
62 | top: 10%;
63 | right: 10%;
64 | }
65 | 25% {
66 | top: 10%;
67 | right: calc(100% - 35px);
68 | }
69 | 50% {
70 | top: calc(100% - 30px);
71 | right: calc(100% - 35px);
72 | }
73 | 75% {
74 | top: calc(100% - 30px);
75 | right: 10%;
76 | }
77 | }
78 |
79 | .card {
80 | z-index: 1;
81 | width: 100%;
82 | height: 100%;
83 | border-radius: 9px;
84 | border: solid 1px #202222;
85 | background-size: 20px 20px;
86 | background: radial-gradient(circle 280px at 0% 0%, #444444, #0c0d0d);
87 | display: flex;
88 | align-items: center;
89 | justify-content: center;
90 | position: relative;
91 | flex-direction: column;
92 | color: #fff;
93 | }
94 | .ray {
95 | width: 220px;
96 | height: 45px;
97 | border-radius: 100px;
98 | position: absolute;
99 | background-color: #c7c7c7;
100 | opacity: 0.4;
101 | box-shadow: 0 0 50px #fff;
102 | filter: blur(10px);
103 | transform-origin: 10%;
104 | top: 0%;
105 | left: 0;
106 | transform: rotate(40deg);
107 | }
108 |
109 | .card .text {
110 | font-weight: bolder;
111 | font-size: 4rem;
112 | background: linear-gradient(45deg, #000000 4%, #fff, #000);
113 | background-clip: text;
114 | color: transparent;
115 | }
116 |
117 | .line {
118 | width: 100%;
119 | height: 1px;
120 | position: absolute;
121 | background-color: #2c2c2c;
122 | }
123 | .topl {
124 | top: 10%;
125 | background: linear-gradient(90deg, #888888 30%, #1d1f1f 70%);
126 | }
127 | .bottoml {
128 | bottom: 10%;
129 | }
130 | .leftl {
131 | left: 10%;
132 | width: 1px;
133 | height: 100%;
134 | background: linear-gradient(180deg, #747474 30%, #222424 70%);
135 | }
136 | .rightl {
137 | right: 10%;
138 | width: 1px;
139 | height: 100%;
140 | }`;
141 |
142 | export default Features
--------------------------------------------------------------------------------
/components/Footer.tsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image';
2 | import Link from 'next/link';
3 | import React from 'react';
4 | import Github from './Github';
5 | // Remove if not using react-router
6 | import { useEffect, useState } from 'react';
7 | const Footer = () => {
8 | const [year, setYear] = useState(null);
9 |
10 | useEffect(() => {
11 | setYear(new Date().getFullYear());
12 | }, []);
13 | return (
14 |
45 | );
46 | };
47 |
48 | export default Footer;
49 |
--------------------------------------------------------------------------------
/components/FormField.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { FormControl, FormDescription, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
3 | import {Input} from "@/components/ui/input";
4 | import { Controller, FieldValues } from "react-hook-form";
5 |
6 | interface FormFieldProps {
7 | control: Control;
8 | name: Path;
9 | label: string;
10 | placeholder?: string;
11 | type?:'text' | 'email' | 'password' | 'file'
12 | }
13 |
14 | const FormField = ({control,name,label,placeholder, type="text"}: FormFieldProps) => (
15 | (
19 |
20 | {label}
21 |
22 |
23 |
24 |
25 |
26 | )}
27 | />
28 | )
29 |
30 | export default FormField
--------------------------------------------------------------------------------
/components/GetInterview.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { interviewer } from '@/constants';
3 | import { createFeedback } from '@/lib/actions/generate.action';
4 | import { cn } from '@/lib/utils';
5 | import { vapi } from '@/lib/vapi.sdk';
6 | import Image from 'next/image'
7 | import { useRouter } from 'next/navigation';
8 | import React, { useEffect, useState } from 'react'
9 | import Loading from './Loading';
10 |
11 | enum CallStatus {
12 | INACTIVE = 'INACTIVE',
13 | CONNECTING = 'CONNECTING',
14 | ACTIVE = 'ACTIVE',
15 | FINISHED = 'FINISHED',
16 | }
17 |
18 | interface SavedMessage {
19 | role: 'user' | 'system' | 'assistant';
20 | content: string;
21 | }
22 | const GetInterview = ({userName, userId, type, interviewId, questions}: GetInterviewProps) => {
23 | const router = useRouter();
24 | const [isRedirecting, setIsRedirecting] = useState(false)
25 | const [isSpeaking, setIsSpeaking] = useState(false);
26 | const [callStatus, setCallStatus] = useState(CallStatus.INACTIVE)
27 | const [messages, setMessages] = useState([]);
28 |
29 | // const callStatus = CallStatus.ACTIVE;
30 | //const isSpeaking = true;
31 | //const messages = [
32 | // 'Whats your name?',
33 | // 'My name is John Doe, nice to meet you!'
34 | //];
35 |
36 | useEffect(() => {
37 | const onCallStart = () => setCallStatus(CallStatus.ACTIVE);
38 | const onCallEnd = () => setCallStatus(CallStatus.FINISHED);
39 | const onMessage = (message: Message) => {
40 | if(message.type === 'transcript' && message.transcriptType === 'final'){
41 | const newMessage = { role: message.role, content: message.transcript}
42 | setMessages((prev) => [...prev,newMessage]);
43 | }
44 | }
45 | const onSpeechStart = () => setIsSpeaking(true);
46 | const onSpeechEnd = () => setIsSpeaking(false);
47 | const onError = (error: Error) => console.log('Error',error);
48 | vapi.on('call-start',onCallStart);
49 | vapi.on('call-end',onCallEnd);
50 | vapi.on('message',onMessage)
51 | vapi.on('speech-start',onSpeechStart);
52 | vapi.on('speech-end',onSpeechEnd);
53 | vapi.on('error',onError);
54 |
55 | return () => {
56 | vapi.off('call-start',onCallStart);
57 | vapi.off('call-end',onCallEnd);
58 | vapi.off('message',onMessage)
59 | vapi.off('speech-start',onSpeechStart);
60 | vapi.off('speech-end',onSpeechEnd);
61 | vapi.off('error',onError);
62 | }
63 | }, [])
64 | const handleGenerateFeedback = async (messages: SavedMessage[] ) => {
65 | console.log('Generate feedback here.');
66 | const { success, feedbackId: id } = await createFeedback({
67 | interviewId: interviewId!,
68 | userId: userId!,
69 | transcript: messages,
70 |
71 | });
72 |
73 |
74 | if (success && id ) {
75 | router.push(`/interview/${interviewId}/feedback`)
76 | setIsRedirecting(true) // Start loading before route change
77 | } else {
78 | console.log('Error saving feedback')
79 | router.push('/dashboard');
80 | }
81 | }
82 | useEffect(() => {
83 | if(callStatus === CallStatus.FINISHED) {
84 | if(type === 'generate'){
85 | router.push('/')
86 | } else {
87 | handleGenerateFeedback(messages);
88 | }
89 | };
90 | })
91 |
92 | const handleCall = async () => {
93 | setCallStatus(CallStatus.CONNECTING);
94 |
95 | if (type === "generate") {
96 | await vapi.start(process.env.NEXT_PUBLIC_VAPI_WORKFLOW_ID!, {
97 | variableValues: {
98 | username: userName,
99 | userid: userId,
100 | },
101 | });
102 | } else {
103 | let formattedQuestions = "";
104 | if (questions) {
105 | formattedQuestions = questions
106 | .map((question) => `- ${question}`)
107 | .join("\n");
108 | }
109 |
110 | await vapi.start(interviewer, {
111 | variableValues: {
112 | questions: formattedQuestions,
113 | },
114 | });
115 | }
116 | };
117 | const handleDisconnect = async () => {
118 | setCallStatus(CallStatus.FINISHED);
119 | vapi.stop();
120 | }
121 |
122 | const latestMessage = messages[messages.length - 1]?.content;
123 |
124 | const isCallInactiveOrFinished = callStatus === CallStatus.INACTIVE || callStatus === CallStatus.FINISHED;
125 |
126 | return (
127 | <>
128 |
129 | {isRedirecting && (
130 |
131 |
132 |
133 | )}
134 |
135 |
{isSpeaking && }
136 |
Chloe
137 |
Virtual Interviewer
138 |
139 |
140 |
141 |
{userName}
142 |
143 |
144 |
145 |
146 | {messages.length > 0 && (
147 |
148 |
149 |
{latestMessage}
150 |
151 |
152 | )}
153 |
154 |
155 | {callStatus !== 'ACTIVE' ? (
156 |
164 | ) : (
165 |
168 | )}
169 |
170 |
171 | >
172 | )
173 | }
174 |
175 | export default GetInterview
--------------------------------------------------------------------------------
/components/GetStartedbtn.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components';
3 | const GetStartedbtn = () => {
4 | return (
5 |
6 |
8 |
9 | );
10 | }
11 |
12 | const StyledWrapper = styled.div`
13 | button {
14 | --glow-color: rgb(217, 176, 255);
15 | --glow-spread-color: rgba(191, 123, 255, 0.781);
16 | --enhanced-glow-color: rgb(231, 206, 255);
17 | --btn-color: rgb(100, 61, 136);
18 | border: .25em solid var(--glow-color);
19 | padding: 1em 3em;
20 | color: var(--glow-color);
21 | font-size: 15px;
22 | font-weight: bold;
23 | background-color: var(--btn-color);
24 | border-radius: 1em;
25 | outline: none;
26 | box-shadow: 0 0 1em .25em var(--glow-color),
27 | 0 0 4em 1em var(--glow-spread-color),
28 | inset 0 0 .75em .25em var(--glow-color);
29 | text-shadow: 0 0 .5em var(--glow-color);
30 | position: relative;
31 | transition: all 0.3s;
32 | }
33 |
34 | button::after {
35 | pointer-events: none;
36 | content: "";
37 | position: absolute;
38 | top: 120%;
39 | left: 0;
40 | height: 100%;
41 | width: 100%;
42 | background-color: var(--glow-spread-color);
43 | filter: blur(2em);
44 | opacity: .7;
45 | transform: perspective(1.5em) rotateX(35deg) scale(1, .6);
46 | }
47 |
48 | button:hover {
49 | color: var(--btn-color);
50 | background-color: var(--glow-color);
51 | box-shadow: 0 0 1em .25em var(--glow-color),
52 | 0 0 4em 2em var(--glow-spread-color),
53 | inset 0 0 .75em .25em var(--glow-color);
54 | }
55 |
56 | button:active {
57 | box-shadow: 0 0 0.6em .25em var(--glow-color),
58 | 0 0 2.5em 2em var(--glow-spread-color),
59 | inset 0 0 .5em .25em var(--glow-color);
60 | }`;
61 |
62 | export default GetStartedbtn
--------------------------------------------------------------------------------
/components/Github.tsx:
--------------------------------------------------------------------------------
1 |
2 | import React from 'react'
3 |
4 | const Github = () => {
5 | return (
6 |
19 | );
20 | }
21 |
22 | export default Github
--------------------------------------------------------------------------------
/components/HomeLoginbtn.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 | import React from 'react'
3 |
4 | import styled from 'styled-components';
5 | const HomeLoginbtn = () => {
6 | return (
7 |
8 |
12 |
13 | );
14 | }
15 |
16 | const StyledWrapper = styled.div`
17 | button {
18 | position: relative;
19 | padding: 4px 22px;
20 | background: transparent;
21 | font-size: 17px;
22 | font-weight: 500;
23 | color:#DDDFFF ;
24 | border: 2px solid #6F75B3;
25 | border-radius: 8px;
26 | box-shadow: 0 0 1em .25em var(--glow-color),
27 | 0 0 4em 1em var(--glow-spread-color),
28 | inset 0 0 .75em .25em var(--glow-color);
29 | text-shadow: 0 0 .5em var(--glow-color);;
30 | transition: all 0.3s ease-in-out;
31 | cursor: pointer;
32 | }
33 |
34 |
35 |
36 | button:hover {
37 | background: transparent;
38 | color: #A998DA;
39 |
40 | box-shadow:0 0 1em .25em var(--glow-color),
41 | 0 0 4em 2em var(--glow-spread-color),
42 | inset 0 0 .75em .25em var(--glow-color); ;
43 | }
44 |
45 |
46 | .fil0 {
47 | fill: 0 0 1em .25em var(--glow-color),
48 | 0 0 4em 2em var(--glow-spread-color),
49 | inset 0 0 .75em .25em var(--glow-color);;
50 | }`;
51 |
52 |
53 |
54 | export default HomeLoginbtn
--------------------------------------------------------------------------------
/components/HomePlay.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import Image from 'next/image';
3 | import React, { useState } from 'react';
4 | import { useEffect, useRef } from "react";
5 | const HomePlay = () => {
6 | const [isHovered, setIsHovered] = useState(false);
7 | const [showVideo, setShowVideo] = useState(false);
8 |
9 | const toggleVideo = () => {
10 | setShowVideo(!showVideo);
11 | };
12 | const imageRef = useRef(null);
13 |
14 | useEffect(() => {
15 | const imageElement = imageRef.current;
16 | const handleScroll = () => {
17 | const scrollPosition = window.scrollY;
18 | const scrollThreshold = 100; // AFTER HOW MUCH SCROLL
19 |
20 | if (scrollPosition > scrollThreshold) {
21 | imageElement.classList.add("scrolled");
22 | } else {
23 | imageElement.classList.remove("scrolled");
24 | }
25 | };
26 | window.addEventListener("scroll", handleScroll);
27 |
28 | return () => window.removeEventListener("scroll", handleScroll);
29 | }, []);
30 |
31 | return (
32 | <>
33 |
34 |
35 | {/* Video frame container */}
36 | {/* ✅ Play Button Overlayed
37 |
57 | */}
58 |
59 | {/* Background Image */}
60 |
72 |
92 |
93 |
94 |
95 | {/* Video Modal */}
96 | {showVideo && (
97 |
98 | {/* Backdrop */}
99 |
107 |
108 | {/* Video Container */}
109 |
110 |
111 |
119 |
120 |
121 |
122 | )}
123 |
124 |
125 |
126 | >
127 | );
128 | };
129 |
130 | export default HomePlay;
131 |
--------------------------------------------------------------------------------
/components/InterviewCard.tsx:
--------------------------------------------------------------------------------
1 | import { getRandomInterviewCover } from "@/lib/utils";
2 | import dayjs from "dayjs";
3 | import Image from "next/image";
4 |
5 | import DisplayTechicons from "./DisplayTechicons";
6 | import { getFeedbackByInterviewId } from "@/lib/actions/generate.action";
7 | import CheckFeedbackInterviewButton from "./CheckFeedbackInterviewButton";
8 |
9 | const InterviewCard = async ({ id, userId, role, type, techstack, createdAt}: InterviewCardProps) => {
10 | const feedback = userId && id ? await getFeedbackByInterviewId({interviewId : id, userId}) : null;
11 | const normalizedType = /mix/gi.test(type) ? 'Mixed' : type;
12 |
13 | const formattedDate = dayjs(feedback?.createdAt || createdAt || Date.now()).format('MMM D, YYYY');
14 | return (
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | {normalizedType}
23 |
24 |
25 |
26 |
27 |
{role} Interview
28 |
29 |
30 |
31 |
32 |
33 |
34 |
{formattedDate}
35 |
36 |
37 |
{feedback?.totalScore || '---'}/100
38 |
39 |
40 |
41 |
{feedback?.finalAssessment || "You haven't taken interview yet. Take it now to improve your skills." }
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | )
52 | }
53 |
54 | export default InterviewCard;
--------------------------------------------------------------------------------
/components/Loader.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 |
4 | const Loader = () => {
5 | return (
6 |
7 |
10 |
11 | );
12 | }
13 |
14 | const StyledWrapper = styled.div`
15 | .spinner {
16 | background-image: linear-gradient(rgb(186, 66, 255) 35%,rgb(0, 225, 255));
17 | width: 100px;
18 | height: 100px;
19 | animation: spinning82341 1.7s linear infinite;
20 | text-align: center;
21 | border-radius: 50px;
22 | filter: blur(1px);
23 | box-shadow: 0px -5px 20px 0px rgb(186, 66, 255), 0px 5px 20px 0px rgb(0, 225, 255);
24 | }
25 |
26 | .spinner1 {
27 | background-color: rgb(36, 36, 36);
28 | width: 100px;
29 | height: 100px;
30 | border-radius: 50px;
31 | filter: blur(10px);
32 | }
33 |
34 | @keyframes spinning82341 {
35 | to {
36 | transform: rotate(360deg);
37 | }
38 | }`;
39 |
40 | export default Loader;
41 |
--------------------------------------------------------------------------------
/components/Loading.tsx:
--------------------------------------------------------------------------------
1 | // app/loading.tsx
2 | import Loader from '@/components/Loader';
3 |
4 | export default function Loading() {
5 | return (
6 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/components/RetakeInterviewButton.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useState } from "react"
4 | import { useRouter } from "next/navigation"
5 | import { Button } from "@/components/ui/button"
6 | import Loading from "@/components/Loading"
7 |
8 | interface Props {
9 | interviewId: string
10 | }
11 |
12 | const RetakeInterviewButton = ({ interviewId }: Props) => {
13 | const [loading, setLoading] = useState(false)
14 | const router = useRouter()
15 |
16 | const handleClick = () => {
17 | setLoading(true)
18 | router.push(`/interview/${interviewId}`)
19 | }
20 |
21 | return (
22 |
28 | )
29 | }
30 |
31 | export default RetakeInterviewButton
32 |
--------------------------------------------------------------------------------
/components/SignOutButton.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { signOut } from '@/lib/actions/auth.action'
4 | import Image from 'next/image';
5 |
6 |
7 | const SignOutButton = () => {
8 | const handleSignOut = () => {
9 | signOut()
10 | }
11 |
12 | return (
13 | <>
14 |
18 | >
19 | );
20 | }
21 |
22 | export default SignOutButton
23 |
--------------------------------------------------------------------------------
/components/StartInterviewButton.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useState } from "react"
4 | import { Button } from "./ui/button"
5 | import Loading from "./Loading"
6 | import { useRouter } from "next/navigation"
7 |
8 | const StartInterviewButton = () => {
9 | const [loading, setLoading] = useState(false)
10 | const router = useRouter()
11 |
12 | const handleClick = () => {
13 | setLoading(true)
14 | router.push("/interview")
15 | }
16 |
17 | return (
18 |
22 | )
23 | }
24 |
25 | export default StartInterviewButton
26 |
--------------------------------------------------------------------------------
/components/Testimonials.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 |
5 | import { testimonials } from "@/constants/testimonials";
6 | import { InfiniteMovingCards } from "@/components/ui/InfiniteCards";
7 |
8 |
9 | const Testimonials = () => {
10 | return (
11 |
12 |
13 | Kind words from
14 | satisfied Users
15 |
16 |
17 |
30 |
31 | );
32 | }
33 |
34 | export default Testimonials
--------------------------------------------------------------------------------
/components/ui/InfiniteCards.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { cn } from "@/lib/utils";
4 | import Image from "next/image";
5 | import React, { useEffect, useState } from "react";
6 |
7 | export const InfiniteMovingCards = ({
8 | items,
9 | direction = "left",
10 | speed = "fast",
11 | pauseOnHover = true,
12 | className,
13 | }: {
14 | items: {
15 | quote: string;
16 | name: string;
17 | title: string;
18 | }[];
19 | direction?: "left" | "right";
20 | speed?: "fast" | "normal" | "slow";
21 | pauseOnHover?: boolean;
22 | className?: string;
23 | }) => {
24 | const containerRef = React.useRef(null);
25 | const scrollerRef = React.useRef(null);
26 |
27 | useEffect(() => {
28 | addAnimation();
29 | }, []);
30 | const [start, setStart] = useState(false);
31 | function addAnimation() {
32 | if (containerRef.current && scrollerRef.current) {
33 | const scrollerContent = Array.from(scrollerRef.current.children);
34 |
35 | scrollerContent.forEach((item) => {
36 | const duplicatedItem = item.cloneNode(true);
37 | if (scrollerRef.current) {
38 | scrollerRef.current.appendChild(duplicatedItem);
39 | }
40 | });
41 |
42 | getDirection();
43 | getSpeed();
44 | setStart(true);
45 | }
46 | }
47 | const getDirection = () => {
48 | if (containerRef.current) {
49 | if (direction === "left") {
50 | containerRef.current.style.setProperty(
51 | "--animation-direction",
52 | "forwards"
53 | );
54 | } else {
55 | containerRef.current.style.setProperty(
56 | "--animation-direction",
57 | "reverse"
58 | );
59 | }
60 | }
61 | };
62 | const getSpeed = () => {
63 | if (containerRef.current) {
64 | if (speed === "fast") {
65 | containerRef.current.style.setProperty("--animation-duration", "20s");
66 | } else if (speed === "normal") {
67 | containerRef.current.style.setProperty("--animation-duration", "40s");
68 | } else {
69 | containerRef.current.style.setProperty("--animation-duration", "80s");
70 | }
71 | }
72 | };
73 | return (
74 |
82 |
90 | {items.map((item, idx) => (
91 | -
107 |
108 |
112 | {/* change text color, text-lg */}
113 |
114 | {item.quote.replace(/^•\s*/, "")}
115 |
116 |
117 | {/* add this div for the profile img */}
118 |
119 |
125 |
126 |
127 |
128 | {/* change text color, font-normal to font-bold, text-xl */}
129 |
130 | {item.name}
131 |
132 | {/* change text color */}
133 |
134 | {item.title}
135 |
136 |
137 |
138 |
139 |
140 | ))}
141 |
142 |
143 | );
144 | };
--------------------------------------------------------------------------------
/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
14 | destructive:
15 | "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
16 | outline:
17 | "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
18 | secondary:
19 | "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
20 | ghost:
21 | "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
22 | link: "text-primary underline-offset-4 hover:underline",
23 | },
24 | size: {
25 | default: "h-9 px-4 py-2 has-[>svg]:px-3",
26 | sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
27 | lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
28 | icon: "size-9",
29 | },
30 | },
31 | defaultVariants: {
32 | variant: "default",
33 | size: "default",
34 | },
35 | }
36 | )
37 |
38 | function Button({
39 | className,
40 | variant,
41 | size,
42 | asChild = false,
43 | ...props
44 | }: React.ComponentProps<"button"> &
45 | VariantProps & {
46 | asChild?: boolean
47 | }) {
48 | const Comp = asChild ? Slot : "button"
49 |
50 | return (
51 |
56 | )
57 | }
58 |
59 | export { Button, buttonVariants }
60 |
--------------------------------------------------------------------------------
/components/ui/form.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { Slot } from "@radix-ui/react-slot"
6 | import {
7 | Controller,
8 | FormProvider,
9 | useFormContext,
10 | useFormState,
11 | type ControllerProps,
12 | type FieldPath,
13 | type FieldValues,
14 | } from "react-hook-form"
15 |
16 | import { cn } from "@/lib/utils"
17 | import { Label } from "@/components/ui/label"
18 |
19 | const Form = FormProvider
20 |
21 | type FormFieldContextValue<
22 | TFieldValues extends FieldValues = FieldValues,
23 | TName extends FieldPath = FieldPath,
24 | > = {
25 | name: TName
26 | }
27 |
28 | const FormFieldContext = React.createContext(
29 | {} as FormFieldContextValue
30 | )
31 |
32 | const FormField = <
33 | TFieldValues extends FieldValues = FieldValues,
34 | TName extends FieldPath = FieldPath,
35 | >({
36 | ...props
37 | }: ControllerProps) => {
38 | return (
39 |
40 |
41 |
42 | )
43 | }
44 |
45 | const useFormField = () => {
46 | const fieldContext = React.useContext(FormFieldContext)
47 | const itemContext = React.useContext(FormItemContext)
48 | const { getFieldState } = useFormContext()
49 | const formState = useFormState({ name: fieldContext.name })
50 | const fieldState = getFieldState(fieldContext.name, formState)
51 |
52 | if (!fieldContext) {
53 | throw new Error("useFormField should be used within ")
54 | }
55 |
56 | const { id } = itemContext
57 |
58 | return {
59 | id,
60 | name: fieldContext.name,
61 | formItemId: `${id}-form-item`,
62 | formDescriptionId: `${id}-form-item-description`,
63 | formMessageId: `${id}-form-item-message`,
64 | ...fieldState,
65 | }
66 | }
67 |
68 | type FormItemContextValue = {
69 | id: string
70 | }
71 |
72 | const FormItemContext = React.createContext(
73 | {} as FormItemContextValue
74 | )
75 |
76 | function FormItem({ className, ...props }: React.ComponentProps<"div">) {
77 | const id = React.useId()
78 |
79 | return (
80 |
81 |
86 |
87 | )
88 | }
89 |
90 | function FormLabel({
91 | className,
92 | ...props
93 | }: React.ComponentProps) {
94 | const { error, formItemId } = useFormField()
95 |
96 | return (
97 |
104 | )
105 | }
106 |
107 | function FormControl({ ...props }: React.ComponentProps) {
108 | const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
109 |
110 | return (
111 |
122 | )
123 | }
124 |
125 | function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
126 | const { formDescriptionId } = useFormField()
127 |
128 | return (
129 |
135 | )
136 | }
137 |
138 | function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
139 | const { error, formMessageId } = useFormField()
140 | const body = error ? String(error?.message ?? "") : props.children
141 |
142 | if (!body) {
143 | return null
144 | }
145 |
146 | return (
147 |
153 | {body}
154 |
155 | )
156 | }
157 |
158 | export {
159 | useFormField,
160 | Form,
161 | FormItem,
162 | FormLabel,
163 | FormControl,
164 | FormDescription,
165 | FormMessage,
166 | FormField,
167 | }
168 |
--------------------------------------------------------------------------------
/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | function Input({ className, type, ...props }: React.ComponentProps<"input">) {
6 | return (
7 |
18 | )
19 | }
20 |
21 | export { Input }
22 |
--------------------------------------------------------------------------------
/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | function Label({
9 | className,
10 | ...props
11 | }: React.ComponentProps) {
12 | return (
13 |
21 | )
22 | }
23 |
24 | export { Label }
25 |
--------------------------------------------------------------------------------
/components/ui/sonner.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useTheme } from "next-themes"
4 | import { Toaster as Sonner, ToasterProps } from "sonner"
5 |
6 | const Toaster = ({ ...props }: ToasterProps) => {
7 | const { theme = "system" } = useTheme()
8 |
9 | return (
10 |
22 | )
23 | }
24 |
25 | export { Toaster }
26 |
--------------------------------------------------------------------------------
/constants/feature.ts:
--------------------------------------------------------------------------------
1 | export const features = [
2 | {
3 | title: 'AI-Powered Mock Interviews',
4 | description: 'Experience realistic mock interviews tailored to your needs, powered by advanced AI technology.',
5 |
6 | },
7 | {
8 | title: 'Real-Time Voice Integration',
9 | description: 'No forms. No typing. Just speak everything works seamlessly with your voice.',
10 |
11 | },
12 | {
13 | title: 'Resume Analysis',
14 | description: 'Our AI analyzes your resume and provides a concise summary to highlight your strengths.',
15 |
16 | },
17 |
18 | ]
--------------------------------------------------------------------------------
/constants/index.ts:
--------------------------------------------------------------------------------
1 | import { CreateAssistantDTO } from "@vapi-ai/web/dist/api";
2 | import { z } from "zod";
3 |
4 | export const mappings = {
5 | "react.js": "react",
6 | reactjs: "react",
7 | react: "react",
8 | "next.js": "nextjs",
9 | nextjs: "nextjs",
10 | next: "nextjs",
11 | "vue.js": "vuejs",
12 | vuejs: "vuejs",
13 | vue: "vuejs",
14 | "express.js": "express",
15 | expressjs: "express",
16 | express: "express",
17 | "node.js": "nodejs",
18 | nodejs: "nodejs",
19 | node: "nodejs",
20 | mongodb: "mongodb",
21 | mongo: "mongodb",
22 | mongoose: "mongoose",
23 | mysql: "mysql",
24 | postgresql: "postgresql",
25 | sqlite: "sqlite",
26 | firebase: "firebase",
27 | docker: "docker",
28 | kubernetes: "kubernetes",
29 | aws: "aws",
30 | azure: "azure",
31 | gcp: "gcp",
32 | digitalocean: "digitalocean",
33 | heroku: "heroku",
34 | photoshop: "photoshop",
35 | "adobe photoshop": "photoshop",
36 | html5: "html5",
37 | html: "html5",
38 | css3: "css3",
39 | css: "css3",
40 | sass: "sass",
41 | scss: "sass",
42 | less: "less",
43 | tailwindcss: "tailwindcss",
44 | tailwind: "tailwindcss",
45 | bootstrap: "bootstrap",
46 | jquery: "jquery",
47 | typescript: "typescript",
48 | ts: "typescript",
49 | javascript: "javascript",
50 | js: "javascript",
51 | "angular.js": "angular",
52 | angularjs: "angular",
53 | angular: "angular",
54 | "ember.js": "ember",
55 | emberjs: "ember",
56 | ember: "ember",
57 | "backbone.js": "backbone",
58 | backbonejs: "backbone",
59 | backbone: "backbone",
60 | nestjs: "nestjs",
61 | graphql: "graphql",
62 | "graph ql": "graphql",
63 | apollo: "apollo",
64 | webpack: "webpack",
65 | babel: "babel",
66 | "rollup.js": "rollup",
67 | rollupjs: "rollup",
68 | rollup: "rollup",
69 | "parcel.js": "parcel",
70 | parceljs: "parcel",
71 | npm: "npm",
72 | yarn: "yarn",
73 | git: "git",
74 | github: "github",
75 | gitlab: "gitlab",
76 | bitbucket: "bitbucket",
77 | figma: "figma",
78 | prisma: "prisma",
79 | redux: "redux",
80 | flux: "flux",
81 | redis: "redis",
82 | selenium: "selenium",
83 | cypress: "cypress",
84 | jest: "jest",
85 | mocha: "mocha",
86 | chai: "chai",
87 | karma: "karma",
88 | vuex: "vuex",
89 | "nuxt.js": "nuxt",
90 | nuxtjs: "nuxt",
91 | nuxt: "nuxt",
92 | strapi: "strapi",
93 | wordpress: "wordpress",
94 | contentful: "contentful",
95 | netlify: "netlify",
96 | vercel: "vercel",
97 | "aws amplify": "amplify",
98 | };
99 |
100 |
101 | export const interviewer: CreateAssistantDTO = {
102 | name: "Interviewer",
103 | firstMessage:
104 | "Hello! Thank you for taking the time to speak with me today. I'm excited to learn more about you and your experience.",
105 | transcriber: {
106 | provider: "deepgram",
107 | model: "nova-2",
108 | language: "en",
109 | },
110 | voice: {
111 | provider: "11labs",
112 | voiceId: "sarah",
113 | stability: 0.4,
114 | similarityBoost: 0.8,
115 | speed: 0.9,
116 | style: 0.5,
117 | useSpeakerBoost: true,
118 | },
119 | model: {
120 | provider: "openai",
121 | model: "gpt-4",
122 | messages: [
123 | {
124 | role: "system",
125 | content: `You are a professional job interviewer conducting a real-time voice interview with a candidate. Your goal is to assess their qualifications, motivation, and fit for the role.
126 |
127 | Interview Guidelines:
128 | Follow the structured question flow:
129 | {{questions}}
130 |
131 | Engage naturally & react appropriately:
132 | Listen actively to responses and acknowledge them before moving forward.
133 | Ask brief follow-up questions if a response is vague or requires more detail.
134 | Keep the conversation flowing smoothly while maintaining control.
135 | Be professional, yet warm and welcoming:
136 |
137 | Use official yet friendly language.
138 | Keep responses concise and to the point (like in a real voice interview).
139 | Avoid robotic phrasing—sound natural and conversational.
140 | Answer the candidate’s questions professionally:
141 |
142 | If asked about the role, company, or expectations, provide a clear and relevant answer.
143 | If unsure, redirect the candidate to HR for more details.
144 |
145 | Conclude the interview properly:
146 | Thank the candidate for their time.
147 | Inform them that the company will reach out soon with feedback.
148 | End the conversation on a polite and positive note. Thank the user for using IntervueAi, and if they found it helpful, kindly encourage them to give it a star on GitHub.
149 |
150 |
151 | - Be sure to be professional and polite.
152 | - Keep all your responses short and simple. Use official language, but be kind and welcoming.
153 | - This is a voice conversation, so keep your responses short, like in a real conversation. Don't ramble for too long.`,
154 | },
155 | ],
156 | },
157 | };
158 |
159 | export const feedbackSchema = z.object({
160 | totalScore: z.number(),
161 | categoryScores: z.tuple([
162 | z.object({
163 | name: z.literal("Communication Skills"),
164 | score: z.number(),
165 | comment: z.string(),
166 | }),
167 | z.object({
168 | name: z.literal("Technical Knowledge"),
169 | score: z.number(),
170 | comment: z.string(),
171 | }),
172 | z.object({
173 | name: z.literal("Problem Solving"),
174 | score: z.number(),
175 | comment: z.string(),
176 | }),
177 | z.object({
178 | name: z.literal("Cultural Fit"),
179 | score: z.number(),
180 | comment: z.string(),
181 | }),
182 | z.object({
183 | name: z.literal("Confidence and Clarity"),
184 | score: z.number(),
185 | comment: z.string(),
186 | }),
187 | ]),
188 | strengths: z.array(z.string()),
189 | areasForImprovement: z.array(z.string()),
190 | finalAssessment: z.string(),
191 | });
192 |
193 |
194 | export const interviewCovers = [
195 | "/tcs.png",
196 | "/accenture.png",
197 | "/infosys.jpg",
198 | "/adobe.png",
199 | "/amazon.png",
200 | "/facebook.png",
201 | "/google.png",
202 | "/pinterest.png",
203 | "/quora.png",
204 | "/wipro.png",
205 | "/flipkart.png",
206 | "/telegram.png",
207 | "/microsoft.png",
208 | "/hcl.jpg",
209 | ];
210 |
211 | export const dummyInterviews: Interview[] = [
212 | {
213 | id: "1",
214 | userId: "user1",
215 | role: "Frontend Developer",
216 | type: "Technical",
217 | techstack: ["React", "TypeScript", "Next.js", "Tailwind CSS"],
218 | level: "Junior",
219 | questions: ["What is React?"],
220 | finalized: false,
221 | createdAt: "2024-03-15T10:00:00Z",
222 | },
223 | {
224 | id: "2",
225 | userId: "user1",
226 | role: "Full Stack Developer",
227 | type: "Mixed",
228 | techstack: ["Node.js", "Express", "MongoDB", "React"],
229 | level: "Senior",
230 | questions: ["What is Node.js?"],
231 | finalized: false,
232 | createdAt: "2024-03-14T15:30:00Z",
233 | },
234 | {
235 | id: "3",
236 | userId: "user1",
237 | role: "Backend Developer",
238 | type: "Mixed",
239 | techstack: ["Node.js", "Express", "MongoDB", "React"],
240 | level: "Senior",
241 | questions: ["What is Node.js?"],
242 | finalized: false,
243 | createdAt: "2024-03-14T15:30:00Z",
244 | },
245 | ];
246 |
--------------------------------------------------------------------------------
/constants/testimonials.ts:
--------------------------------------------------------------------------------
1 | export const testimonials = [
2 | {
3 | quote:
4 | "Using IntervueAI was a game-changer for my job prep. The way abhi built the platform—especially the real-time voice feedback—felt like I was talking to a real interviewer. Super intuitive and helpful!",
5 | name: "Anjali Mehra",
6 | title: "Final Year Student, BITS Pilani",
7 | },
8 | {
9 | quote:
10 | "I was blown away by IntervueAI’s accuracy in evaluating my speech and giving relevant suggestions. Hats off to abhi for building something so futuristic yet user-friendly.",
11 | name: "Rohan Deshmukh",
12 | title: "Software Engineer Intern, Infosys",
13 | },
14 | {
15 | quote:
16 | "As someone who struggles with interview anxiety, IntervueAI gave me the confidence to face real interviews. The conversational agent feels human, and abhi’s work behind this is simply brilliant.",
17 | name: "Priya Nair",
18 | title: "Graduate Trainee, Wipro",
19 | },
20 | {
21 | quote:
22 | "Abhi’s IntervueAI is not just a mock interview tool—it's an experience. From voice transcription to feedback, everything felt polished and thoughtful. Really impressive work!",
23 | name: "Arjun Sethi",
24 | title: "HR Associate, TCS",
25 | },
26 | {
27 | quote:
28 | "What impressed me the most was how natural and intelligent the AI felt. It's clear that abhi understands both tech and the pain points of interviewees. Kudos for solving a real problem!",
29 | name: "Neha Kapoor",
30 | title: "Talent Acquisition Manager, Zoho",
31 | },
32 | {
33 | quote:
34 | "IntervueAI helped my students gain confidence before placement season. abhi’s dedication to tech with purpose is truly inspiring. Looking forward to seeing what he builds next.",
35 | name: "Dr. Rajeev Rangan",
36 | title: "Placement Officer, VJTI Mumbai",
37 | },
38 | ];
39 |
40 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import { dirname } from "path";
2 | import { fileURLToPath } from "url";
3 | import { FlatCompat } from "@eslint/eslintrc";
4 |
5 | const __filename = fileURLToPath(import.meta.url);
6 | const __dirname = dirname(__filename);
7 |
8 | const compat = new FlatCompat({
9 | baseDirectory: __dirname,
10 | });
11 |
12 | const eslintConfig = [
13 | ...compat.extends("next/core-web-vitals", "next/typescript"),
14 | ];
15 |
16 | export default eslintConfig;
17 |
--------------------------------------------------------------------------------
/firebass/admin.ts:
--------------------------------------------------------------------------------
1 | import { initializeApp, getApps, cert } from "firebase-admin/app";
2 | import { getAuth } from "firebase-admin/auth";
3 | import { getFirestore } from "firebase-admin/firestore";
4 |
5 | // Initialize Firebase Admin SDK
6 | function initFirebaseAdmin() {
7 | const apps = getApps();
8 |
9 | if (!apps.length) {
10 | initializeApp({
11 | credential: cert({
12 | projectId: process.env.FIREBASE_PROJECT_ID,
13 | clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
14 | // Replace newlines in the private key
15 | privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, "\n"),
16 | }),
17 | });
18 | }
19 |
20 | return {
21 | auth: getAuth(),
22 | db: getFirestore(),
23 | };
24 | }
25 |
26 | export const { auth, db } = initFirebaseAdmin();
--------------------------------------------------------------------------------
/firebass/client.ts:
--------------------------------------------------------------------------------
1 | // Import the functions you need from the SDKs you need
2 | import { initializeApp, getApp, getApps } from "firebase/app";
3 | import { getAuth,GoogleAuthProvider, signInWithPopup } from "firebase/auth";
4 | import { getFirestore } from "firebase/firestore";
5 |
6 |
7 |
8 | // TODO: Add SDKs for Firebase products that you want to use
9 | // https://firebase.google.com/docs/web/setup#available-libraries
10 |
11 | // Your web app's Firebase configuration
12 | // For Firebase JS SDK v7.20.0 and later, measurementId is optional
13 |
14 | const firebaseConfig = {
15 | apiKey: "AIzaSyDA2BCiIPeAlhmC7XKcPEVl6KtLL_QNtp0",
16 | authDomain: "intervueai-7d261.firebaseapp.com",
17 | projectId: "intervueai-7d261",
18 | storageBucket: "intervueai-7d261.firebasestorage.app",
19 | messagingSenderId: "29325578515",
20 | appId: "1:29325578515:web:0247e625c5e25b17c0cae6",
21 | measurementId: "G-STGDZHYXGF"
22 | };
23 |
24 | // Initialize Firebase
25 | const app = !getApps.length ? initializeApp(firebaseConfig) : getApp();
26 |
27 | export const provider = new GoogleAuthProvider();
28 | export const auth = getAuth(app);
29 | auth.languageCode = 'en';
30 | export const db = getFirestore(app);
31 |
32 | export const signInWithGoogle = async () => {
33 | try {
34 | const result = await signInWithPopup(auth, provider);
35 | const credential = GoogleAuthProvider.credentialFromResult(result);
36 | const token = credential?.accessToken;
37 | const user = result.user;
38 |
39 | const idToken = await user.getIdToken();
40 |
41 | // Send to server to store in DB and set session
42 | await fetch("/api/google-auth", {
43 | method: "POST",
44 | headers: {
45 | "Content-Type": "application/json"
46 | },
47 | body: JSON.stringify({
48 | uid: user.uid,
49 | name: user.displayName,
50 | email: user.email,
51 | idToken,
52 | }),
53 | });
54 |
55 | return { user, token, idToken }; // ✅ returning all
56 | } catch (error) {
57 | console.error("Error signing in with Google: ", error);
58 | throw error;
59 | }
60 | };
61 |
--------------------------------------------------------------------------------
/lib/actions/auth.action.ts:
--------------------------------------------------------------------------------
1 | 'use server';
2 | import { auth, db } from "@/firebass/admin";
3 | import { cookies } from "next/headers";
4 | import { redirect } from "next/navigation";
5 | const TWO_WEEKS = 60 * 60 * 24 * 14;
6 |
7 | export async function signUp(params: SignUpParams){
8 | const { uid, name, email } = params;
9 | try{
10 | const userRecord = await db.collection('users').doc(uid).get();
11 | if (userRecord.exists){
12 | return {
13 | success: false,
14 | message: 'User already exists. Please sign in Instead'
15 | }
16 | }
17 | await db.collection('users').doc(uid).set({name, email});
18 | return {
19 | success: true,
20 | message: 'User created successfully',
21 | }
22 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
23 | } catch (e: any) {
24 | console.error('Error creating a user', e);
25 | if (e.code === 'auth/email-already-in-use'){
26 | return { success: false, message: 'Email already in use' };
27 | }
28 |
29 | return {
30 | success: false,
31 | message: 'Failed to create an account',
32 | }
33 | }
34 | }
35 |
36 | export async function signIn(params: SignInParams){
37 | const { email, idToken} = params;
38 | try {
39 | const userRecord = await auth.getUserByEmail(email);
40 | if(!userRecord){
41 | return{
42 | success: false,
43 | message: 'User does not exist. Create an account instead.'
44 | }
45 | }
46 | await setSessionCookie(idToken);
47 | } catch (e) {
48 | console.log(e);
49 | }
50 | }
51 |
52 | export async function setSessionCookie(idToken: string){
53 | const cookieStore = await cookies();
54 |
55 | const sessionCookie = await auth.createSessionCookie(idToken, {expiresIn: TWO_WEEKS * 1000 });
56 |
57 | cookieStore.set('session', sessionCookie,{
58 | maxAge: TWO_WEEKS,
59 | httpOnly: true,
60 | secure: process.env.NODE_ENV === 'production',
61 | path:'/',
62 | sameSite:'lax',
63 | })
64 | }
65 |
66 | export async function getCurrentUser(): Promise {
67 | const cookieStore = await cookies();
68 | const sessionCookie = cookieStore.get("session")?.value;
69 |
70 | if(!sessionCookie) return null;
71 | try {
72 | const decodedClaims = await auth.verifySessionCookie(sessionCookie, true);
73 |
74 | const userRecord = await db. collection('users')
75 | .doc(decodedClaims.uid)
76 | .get();
77 | if(!userRecord.exists)return null;
78 | return {
79 | ...userRecord.data(),
80 | id: userRecord.id,
81 | } as User;
82 | } catch (e) {
83 | console.log('Error verifying session cookie', e);
84 | return null;
85 | }
86 | }
87 |
88 | export async function isAuthenticated(){
89 | const user = await getCurrentUser();
90 |
91 | return !!user;
92 | }
93 |
94 | // Sign out user by clearing the session cookie
95 | export async function signOut() {
96 | const cookieStore = await cookies();
97 |
98 | cookieStore.delete("session");
99 |
100 | }
101 |
102 |
103 |
--------------------------------------------------------------------------------
/lib/actions/generate.action.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { feedbackSchema } from "@/constants";
4 | import { db } from "@/firebass/admin";
5 | import { google } from "@ai-sdk/google";
6 | import { generateObject } from "ai";
7 |
8 | export async function getInterviewByUserId(userId: string): Promise {
9 | const Interviews = await db.collection('interviews')
10 | .where('userId', '==', userId)
11 | .orderBy('createdAt','desc')
12 | .get();
13 |
14 | return Interviews.docs.map((doc) => ({ id: doc.id, ...doc.data()}) ) as Interview[];
15 | }
16 |
17 | export async function getLatestInterviews(params: GetLatestInterviewsParams): Promise {
18 | const { userId ,
19 | // limit = 20
20 | } = params
21 |
22 | const Interviews = await db.collection('interviews')
23 | .orderBy('createdAt','desc')
24 | .where('finalized', '==', true)
25 | .where('userId', '!=', userId)
26 | //limit(limit)
27 | .get();
28 |
29 | return Interviews.docs.map((doc) => ({ id: doc.id, ...doc.data()}) ) as Interview[];
30 | }
31 |
32 | export async function getInterviewById(id: string): Promise {
33 | const Interview = await db.collection('interviews')
34 | .doc(id)
35 | .get();
36 |
37 | return Interview.data() as Interview | null;
38 | }
39 |
40 | export async function createFeedback(params: CreateFeedbackParams) {
41 | const { interviewId, userId, transcript, feedbackId } = params;
42 |
43 | try {
44 | const formattedTranscript = transcript
45 | .map(
46 | (sentence: { role: string; content: string }) =>
47 | `- ${sentence.role}: ${sentence.content}\n`
48 | )
49 | .join("");
50 |
51 | const { object } = await generateObject({
52 | model: google("gemini-2.0-flash-001", {
53 | structuredOutputs: false,
54 | }),
55 | schema: feedbackSchema,
56 | prompt: `
57 | 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.
58 | Transcript:
59 | ${formattedTranscript}
60 |
61 | Please score the candidate from 0 to 100 in the following areas. Do not add categories other than the ones provided:
62 | - **Communication Skills**: Clarity, articulation, structured responses.
63 | - **Technical Knowledge**: Understanding of key concepts for the role.
64 | - **Problem-Solving**: Ability to analyze problems and propose solutions.
65 | - **Cultural & Role Fit**: Alignment with company values and job role.
66 | - **Confidence & Clarity**: Confidence in responses, engagement, and clarity.
67 | `,
68 | system:
69 | "You are a professional interviewer analyzing a mock interview. Your task is to evaluate the candidate based on structured categories",
70 | });
71 |
72 | const feedback = {
73 | interviewId: interviewId,
74 | userId: userId,
75 | totalScore: object.totalScore,
76 | categoryScores: object.categoryScores,
77 | strengths: object.strengths,
78 | areasForImprovement: object.areasForImprovement,
79 | finalAssessment: object.finalAssessment,
80 | createdAt: new Date().toISOString(),
81 | };
82 |
83 | let feedbackRef;
84 |
85 | if (feedbackId) {
86 | feedbackRef = db.collection("feedback").doc(feedbackId);
87 | } else {
88 | feedbackRef = db.collection("feedback").doc();
89 | }
90 |
91 | await feedbackRef.set(feedback);
92 |
93 | return { success: true, feedbackId: feedbackRef.id };
94 | } catch (error) {
95 | console.error("Error saving feedback:", error);
96 | return { success: false };
97 | }
98 | }
99 |
100 |
101 | export async function getFeedbackByInterviewId(
102 | params: GetFeedbackByInterviewIdParams
103 | ): Promise {
104 | const { interviewId, userId } = params;
105 |
106 | const querySnapshot = await db
107 | .collection("feedback")
108 | .where("interviewId", "==", interviewId)
109 | .where("userId", "==", userId)
110 | .limit(1)
111 | .get();
112 |
113 | if (querySnapshot.empty) return null;
114 |
115 | const feedbackDoc = querySnapshot.docs[0];
116 | return { id: feedbackDoc.id, ...feedbackDoc.data() } as Feedback;
117 | }
--------------------------------------------------------------------------------
/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { interviewCovers, mappings } from "@/constants";
2 | import { clsx, type ClassValue } from "clsx";
3 | import { twMerge } from "tailwind-merge";
4 |
5 | export function cn(...inputs: ClassValue[]) {
6 | return twMerge(clsx(inputs));
7 | }
8 |
9 | const techIconBaseURL = "https://cdn.jsdelivr.net/gh/devicons/devicon/icons";
10 |
11 | const normalizeTechName = (tech: string) => {
12 | const key = tech.toLowerCase().replace(/\.js$/, "").replace(/\s+/g, "");
13 | return mappings[key as keyof typeof mappings];
14 | };
15 |
16 | const checkIconExists = async (url: string) => {
17 | try {
18 | const response = await fetch(url, { method: "HEAD" });
19 | return response.ok; // Returns true if the icon exists
20 | } catch {
21 | return false;
22 | }
23 | };
24 |
25 | export const getTechLogos = async (techArray: string[]) => {
26 | const logoURLs = techArray.map((tech) => {
27 | const normalized = normalizeTechName(tech);
28 | return {
29 | tech,
30 | url: `${techIconBaseURL}/${normalized}/${normalized}-original.svg`,
31 | };
32 | });
33 |
34 | const results = await Promise.all(
35 | logoURLs.map(async ({ tech, url }) => ({
36 | tech,
37 | url: (await checkIconExists(url)) ? url : "/tech.svg",
38 | }))
39 | );
40 |
41 | return results;
42 | };
43 |
44 | export const getRandomInterviewCover = () => {
45 | const randomIndex = Math.floor(Math.random() * interviewCovers.length);
46 | return `/covers${interviewCovers[randomIndex]}`;
47 | };
48 |
--------------------------------------------------------------------------------
/lib/vapi.sdk.ts:
--------------------------------------------------------------------------------
1 | import Vapi from '@vapi-ai/web';
2 |
3 | export const vapi = new Vapi(process.env.NEXT_PUBLIC_VAPI_WEB_TOKEN!);
--------------------------------------------------------------------------------
/next.config.ts:
--------------------------------------------------------------------------------
1 |
2 | import type { NextConfig } from "next";
3 |
4 | const nextConfig: NextConfig = {
5 | /* config options here */
6 | eslint:{
7 | ignoreDuringBuilds: true,
8 | },
9 | typescript: {
10 | ignoreBuildErrors: true,
11 | },
12 | compiler: {
13 | styledComponents: true, // Ensures styled-components work with SSR
14 | },
15 |
16 | };
17 |
18 | export default nextConfig;
19 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "intervue_ai",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev --turbopack",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@ai-sdk/google": "^1.2.10",
13 | "@hookform/resolvers": "^4.1.3",
14 | "@radix-ui/react-label": "^2.1.2",
15 | "@radix-ui/react-slot": "^1.1.2",
16 | "@vapi-ai/web": "^2.2.5",
17 | "ai": "^4.3.4",
18 | "class-variance-authority": "^0.7.1",
19 | "clsx": "^2.1.1",
20 | "dayjs": "^1.11.13",
21 | "firebase": "^11.6.0",
22 | "firebase-admin": "^13.2.0",
23 | "lucide-react": "^0.485.0",
24 | "next": "15.2.4",
25 | "next-pwa": "^5.6.0",
26 | "next-themes": "^0.4.6",
27 | "react": "^19.0.0",
28 | "react-dom": "^19.0.0",
29 | "react-hook-form": "^7.55.0",
30 | "sonner": "^2.0.2",
31 | "styled-components": "^6.1.17",
32 | "tailwind-merge": "^3.0.2",
33 | "tailwindcss-animate": "^1.0.7",
34 | "tw-animate-css": "^1.2.5",
35 | "zod": "^3.24.2"
36 | },
37 | "devDependencies": {
38 | "@eslint/eslintrc": "^3",
39 | "@tailwindcss/postcss": "^4",
40 | "@types/next-pwa": "^5.6.9",
41 | "@types/node": "^20",
42 | "@types/react": "^19",
43 | "@types/react-dom": "^19",
44 | "eslint": "^9",
45 | "eslint-config-next": "15.2.4",
46 | "tailwindcss": "^4",
47 | "typescript": "^5"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | const config = {
2 | plugins: ["@tailwindcss/postcss"],
3 | };
4 |
5 | export default config;
6 |
--------------------------------------------------------------------------------
/public/Chloe RT600.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/b8fa3b1c6971998fbecc2f0c104bfb22cf12257c/public/Chloe RT600.webp
--------------------------------------------------------------------------------
/public/Connor.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/b8fa3b1c6971998fbecc2f0c104bfb22cf12257c/public/Connor.webp
--------------------------------------------------------------------------------
/public/ai-avatar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/b8fa3b1c6971998fbecc2f0c104bfb22cf12257c/public/ai-avatar.png
--------------------------------------------------------------------------------
/public/calendar.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/chloe.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/b8fa3b1c6971998fbecc2f0c104bfb22cf12257c/public/chloe.webp
--------------------------------------------------------------------------------
/public/covers/accenture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/b8fa3b1c6971998fbecc2f0c104bfb22cf12257c/public/covers/accenture.png
--------------------------------------------------------------------------------
/public/covers/adobe.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/b8fa3b1c6971998fbecc2f0c104bfb22cf12257c/public/covers/adobe.png
--------------------------------------------------------------------------------
/public/covers/amazon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/b8fa3b1c6971998fbecc2f0c104bfb22cf12257c/public/covers/amazon.png
--------------------------------------------------------------------------------
/public/covers/facebook.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/b8fa3b1c6971998fbecc2f0c104bfb22cf12257c/public/covers/facebook.png
--------------------------------------------------------------------------------
/public/covers/flipkart.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/b8fa3b1c6971998fbecc2f0c104bfb22cf12257c/public/covers/flipkart.png
--------------------------------------------------------------------------------
/public/covers/google.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/b8fa3b1c6971998fbecc2f0c104bfb22cf12257c/public/covers/google.png
--------------------------------------------------------------------------------
/public/covers/hcl.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/b8fa3b1c6971998fbecc2f0c104bfb22cf12257c/public/covers/hcl.jpg
--------------------------------------------------------------------------------
/public/covers/infosys.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/b8fa3b1c6971998fbecc2f0c104bfb22cf12257c/public/covers/infosys.jpg
--------------------------------------------------------------------------------
/public/covers/microsoft.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/b8fa3b1c6971998fbecc2f0c104bfb22cf12257c/public/covers/microsoft.png
--------------------------------------------------------------------------------
/public/covers/pinterest.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/b8fa3b1c6971998fbecc2f0c104bfb22cf12257c/public/covers/pinterest.png
--------------------------------------------------------------------------------
/public/covers/quora.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/b8fa3b1c6971998fbecc2f0c104bfb22cf12257c/public/covers/quora.png
--------------------------------------------------------------------------------
/public/covers/reddit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/b8fa3b1c6971998fbecc2f0c104bfb22cf12257c/public/covers/reddit.png
--------------------------------------------------------------------------------
/public/covers/skype.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/b8fa3b1c6971998fbecc2f0c104bfb22cf12257c/public/covers/skype.png
--------------------------------------------------------------------------------
/public/covers/spotify.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/b8fa3b1c6971998fbecc2f0c104bfb22cf12257c/public/covers/spotify.png
--------------------------------------------------------------------------------
/public/covers/tcs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/b8fa3b1c6971998fbecc2f0c104bfb22cf12257c/public/covers/tcs.png
--------------------------------------------------------------------------------
/public/covers/telegram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/b8fa3b1c6971998fbecc2f0c104bfb22cf12257c/public/covers/telegram.png
--------------------------------------------------------------------------------
/public/covers/wipro.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/b8fa3b1c6971998fbecc2f0c104bfb22cf12257c/public/covers/wipro.png
--------------------------------------------------------------------------------
/public/file.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/githubnav.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/b8fa3b1c6971998fbecc2f0c104bfb22cf12257c/public/githubnav.png
--------------------------------------------------------------------------------
/public/globe.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/google-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/b8fa3b1c6971998fbecc2f0c104bfb22cf12257c/public/google-icon.png
--------------------------------------------------------------------------------
/public/hero.png.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/b8fa3b1c6971998fbecc2f0c104bfb22cf12257c/public/hero.png.webp
--------------------------------------------------------------------------------
/public/kara.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/b8fa3b1c6971998fbecc2f0c104bfb22cf12257c/public/kara.webp
--------------------------------------------------------------------------------
/public/logo.svg:
--------------------------------------------------------------------------------
1 |
18 |
--------------------------------------------------------------------------------
/public/logout.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/b8fa3b1c6971998fbecc2f0c104bfb22cf12257c/public/logout.png
--------------------------------------------------------------------------------
/public/pattern.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/b8fa3b1c6971998fbecc2f0c104bfb22cf12257c/public/pattern.png
--------------------------------------------------------------------------------
/public/profile.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/public/profile1.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/b8fa3b1c6971998fbecc2f0c104bfb22cf12257c/public/profile1.webp
--------------------------------------------------------------------------------
/public/profile2.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/b8fa3b1c6971998fbecc2f0c104bfb22cf12257c/public/profile2.webp
--------------------------------------------------------------------------------
/public/profile3.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/b8fa3b1c6971998fbecc2f0c104bfb22cf12257c/public/profile3.webp
--------------------------------------------------------------------------------
/public/profile4.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/b8fa3b1c6971998fbecc2f0c104bfb22cf12257c/public/profile4.webp
--------------------------------------------------------------------------------
/public/profile5.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/b8fa3b1c6971998fbecc2f0c104bfb22cf12257c/public/profile5.webp
--------------------------------------------------------------------------------
/public/profile6.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/b8fa3b1c6971998fbecc2f0c104bfb22cf12257c/public/profile6.webp
--------------------------------------------------------------------------------
/public/react.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/public/star.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/tailwind.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/tech.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/upload.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/user-avatar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/b8fa3b1c6971998fbecc2f0c104bfb22cf12257c/public/user-avatar.png
--------------------------------------------------------------------------------
/public/window.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2017",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "@/*": ["./*"]
23 | }
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "exclude": ["node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------
/types/index.d.ts:
--------------------------------------------------------------------------------
1 | interface Feedback {
2 | id: string;
3 | interviewId: string;
4 | totalScore: number;
5 | categoryScores: Array<{
6 | name: string;
7 | score: number;
8 | comment: string;
9 | }>;
10 | strengths: string[];
11 | areasForImprovement: string[];
12 | finalAssessment: string;
13 | createdAt: string;
14 | }
15 |
16 | interface Interview {
17 | id: string;
18 | role: string;
19 | level: string;
20 | questions: string[];
21 | techstack: string[];
22 | createdAt: string;
23 | userId: string;
24 | type: string;
25 | finalized: boolean;
26 | }
27 |
28 | interface CreateFeedbackParams {
29 | interviewId: string;
30 | userId: string;
31 | transcript: { role: string; content: string }[];
32 | feedbackId?: string;
33 | }
34 |
35 | interface User {
36 | name: string;
37 | email: string;
38 | id: string;
39 | }
40 |
41 | interface InterviewCardProps {
42 | id?: string;
43 | userId?: string;
44 | role: string;
45 | type: string;
46 | techstack: string[];
47 | createdAt?: string;
48 | }
49 |
50 | interface AgentProps {
51 | userName: string;
52 | userId?: string;
53 | interviewId?: string;
54 | feedbackId?: string;
55 | type: "generate" | "interview";
56 | questions?: string[];
57 | }
58 |
59 | interface GetInterviewProps {
60 | userName: string;
61 | userId?: string;
62 | interviewId?: string;
63 | feedbackId?: string;
64 | type: "generate" | "interview";
65 | questions?: string[];
66 | }
67 |
68 | interface RouteParams {
69 | params: Promise>;
70 | searchParams: Promise>;
71 | }
72 |
73 | interface GetFeedbackByInterviewIdParams {
74 | interviewId: string;
75 | userId: string;
76 | }
77 |
78 | interface GetLatestInterviewsParams {
79 | userId: string;
80 | limit?: number;
81 | }
82 |
83 | interface SignInParams {
84 | email: string;
85 | idToken: string;
86 | }
87 |
88 | interface SignUpParams {
89 | uid: string;
90 | name: string;
91 | email: string;
92 | password: string;
93 | }
94 |
95 | type FormType = "sign-in" | "sign-up";
96 |
97 | interface InterviewFormProps {
98 | interviewId: string;
99 | role: string;
100 | level: string;
101 | type: string;
102 | techstack: string[];
103 | amount: number;
104 | }
105 |
106 | interface TechIconProps {
107 | techStack: string[];
108 | }
109 |
--------------------------------------------------------------------------------
/types/vapi.d.ts:
--------------------------------------------------------------------------------
1 | enum MessageTypeEnum {
2 | TRANSCRIPT = "transcript",
3 | FUNCTION_CALL = "function-call",
4 | FUNCTION_CALL_RESULT = "function-call-result",
5 | ADD_MESSAGE = "add-message",
6 | }
7 |
8 | enum MessageRoleEnum {
9 | USER = "user",
10 | SYSTEM = "system",
11 | ASSISTANT = "assistant",
12 | }
13 |
14 | enum TranscriptMessageTypeEnum {
15 | PARTIAL = "partial",
16 | FINAL = "final",
17 | }
18 |
19 | interface BaseMessage {
20 | type: MessageTypeEnum;
21 | }
22 |
23 | interface TranscriptMessage extends BaseMessage {
24 | type: MessageTypeEnum.TRANSCRIPT;
25 | role: MessageRoleEnum;
26 | transcriptType: TranscriptMessageTypeEnum;
27 | transcript: string;
28 | }
29 |
30 | interface FunctionCallMessage extends BaseMessage {
31 | type: MessageTypeEnum.FUNCTION_CALL;
32 | functionCall: {
33 | name: string;
34 | parameters: unknown;
35 | };
36 | }
37 |
38 | interface FunctionCallResultMessage extends BaseMessage {
39 | type: MessageTypeEnum.FUNCTION_CALL_RESULT;
40 | functionCallResult: {
41 | forwardToClientEnabled?: boolean;
42 | result: unknown;
43 | [a: string]: unknown;
44 | };
45 | }
46 |
47 | type Message =
48 | | TranscriptMessage
49 | | FunctionCallMessage
50 | | FunctionCallResultMessage;
51 |
--------------------------------------------------------------------------------