aki-008 commited on
Commit
893ff0d
·
1 Parent(s): 3a72516

feat: AI interview setup

Browse files
Backend/app/api/v1/api.py CHANGED
@@ -1,5 +1,5 @@
1
  from fastapi import APIRouter
2
- from app.api.v1.endpoints import auth, quiz, notes
3
 
4
  api_router = APIRouter()
5
 
@@ -23,3 +23,9 @@ api_router.include_router(
23
  prefix="/notes",
24
  tags=["notes"]
25
  )
 
 
 
 
 
 
 
1
  from fastapi import APIRouter
2
+ from app.api.v1.endpoints import auth, quiz, notes, vapi_ai
3
 
4
  api_router = APIRouter()
5
 
 
23
  prefix="/notes",
24
  tags=["notes"]
25
  )
26
+
27
+ api_router.include_router(
28
+ vapi_ai.router,
29
+ prefix="/vapi",
30
+ tags=["Voice AI"]
31
+ )
Backend/app/api/v1/endpoints/notes.py CHANGED
@@ -9,7 +9,7 @@ import uuid
9
  from fastapi.responses import StreamingResponse
10
  from chromadb.api.models.Collection import Collection
11
  from pathlib import Path
12
- from llama_index.readers.file import PyMuPDFReader
13
  from llama_index.core.node_parser import SentenceSplitter
14
  from typing import Annotated
15
  import shutil
 
9
  from fastapi.responses import StreamingResponse
10
  from chromadb.api.models.Collection import Collection
11
  from pathlib import Path
12
+ from llama_index.readers.file.pymu_pdf import PyMuPDFReader
13
  from llama_index.core.node_parser import SentenceSplitter
14
  from typing import Annotated
15
  import shutil
Backend/app/api/v1/endpoints/prompts.py CHANGED
@@ -46,3 +46,13 @@ ANSWER KEY RULES
46
 
47
  Strictly follow the JSON structure and generate exactly 10 MCQs.
48
  """
 
 
 
 
 
 
 
 
 
 
 
46
 
47
  Strictly follow the JSON structure and generate exactly 10 MCQs.
48
  """
49
+
50
+ Interviewer_prompt = """
51
+ f"You are an expert technical interviewer conducting an interview for the role of {job_role}. "
52
+ f"The candidate has {experience} years of experience. "
53
+ f"The difficulty level is {level}. "
54
+ f"Start by welcoming {name} and asking a relevant opening question. "
55
+ "Keep your responses concise and conversational. Do not output markdown or code blocks, just speak naturally. "
56
+ "Assess their skills through follow-up questions."
57
+
58
+ """
Backend/app/api/v1/endpoints/vapi_ai.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, HTTPException, Body
2
+ from pydantic import BaseModel
3
+ from typing import Optional
4
+ from .prompts import Interviewer_prompt
5
+ from app.schema.models import VapiConfigRequest
6
+ from app.config import settings
7
+
8
+ router = APIRouter()
9
+
10
+
11
+
12
+ @router.post("/get-vapi-config")
13
+ async def get_vapi_config(config: VapiConfigRequest):
14
+
15
+ system_prompt = Interviewer_prompt.format(name = config.name,
16
+ job_role = config.job_role,
17
+ experience = config.experience,
18
+ level = config.level)
19
+
20
+ return{
21
+ "assistantId": "1184587d-21d7-48f4-8e82-623a2e574324",
22
+ "overrides": {
23
+ "variableValues": {
24
+ "name": config.name,
25
+ "job_role": config.job_role
26
+ },
27
+ "model": {
28
+ "messages": [
29
+ {
30
+ "role": "system",
31
+ "content": system_prompt
32
+ }
33
+ ]
34
+ }
35
+ }
36
+ }
Backend/app/config.py CHANGED
@@ -18,6 +18,9 @@ class Settings(BaseSettings):
18
 
19
  GROQ_API_KEY:str
20
 
 
 
 
21
  class Config:
22
  env_file = ".env"
23
  extra = "ignore" # quiz
 
18
 
19
  GROQ_API_KEY:str
20
 
21
+ VAPI_ASSISTANT_ID: str
22
+ VAPI_PRIVATE_KEY: str
23
+
24
  class Config:
25
  env_file = ".env"
26
  extra = "ignore" # quiz
Backend/app/schema/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
- from app.schema.models import UserCreate, Token, LoginRequest, Quiz_input, QuizOutput, IngestRequest, ChatMessage, AI_chat_input, pdf_input, SessionCreate, SessionResponse, MessageResponse
2
 
3
- __all__ = ["UserCreate", "Token", "LoginRequest", "Quiz_input", "QuizOutput", "IngestRequest", "ChatMessage", "AI_chat_input", "pdf_input", "SessionCreate", "SessionResponse", "MessageResponse"]
 
1
+ from app.schema.models import UserCreate, Token, LoginRequest, Quiz_input, QuizOutput, IngestRequest, ChatMessage, AI_chat_input, pdf_input, SessionCreate, SessionResponse, MessageResponse, VapiConfigRequest
2
 
3
+ __all__ = ["UserCreate", "Token", "LoginRequest", "Quiz_input", "QuizOutput", "IngestRequest", "ChatMessage", "AI_chat_input", "pdf_input", "SessionCreate", "SessionResponse", "MessageResponse", "VapiConfigRequest"]
Backend/app/schema/models.py CHANGED
@@ -85,4 +85,11 @@ class pdf_input(BaseModel):
85
  class NoteInfo(BaseModel):
86
  id: int
87
  filename: str
88
- created_at: datetime
 
 
 
 
 
 
 
 
85
  class NoteInfo(BaseModel):
86
  id: int
87
  filename: str
88
+ created_at: datetime
89
+
90
+
91
+ class VapiConfigRequest(BaseModel):
92
+ name: str
93
+ job_role: str
94
+ experience: str
95
+ level: str
Backend/requirements.txt CHANGED
@@ -23,4 +23,5 @@ llama-index-embeddings-huggingface
23
  groq
24
  websockets
25
  pyaudio
26
- SpeechRecognition
 
 
23
  groq
24
  websockets
25
  pyaudio
26
+ SpeechRecognition
27
+ vapi-python
Frontend/src/pages/AiInterview.tsx CHANGED
@@ -1,50 +1,139 @@
1
- import React, { useState } from "react";
2
- import { Send, Settings, CheckCircle } from "lucide-react";
 
 
 
 
 
 
 
 
 
 
 
 
3
 
4
- // Define the structure for the interview state
5
- type InterviewState = 'config' | 'chat' | 'results';
 
 
 
6
 
7
  const AIInterview: React.FC = () => {
8
- const [interviewState, setInterviewState] = useState<InterviewState>('config');
9
- const [jobRole, setJobRole] = useState('');
10
- const [experience, setExperience] = useState('');
11
- const [level, setLevel] = useState('Medium');
12
-
13
- // Placeholder for the AI Chat bubble/ball
14
- const InterviewBall: React.FC = () => (
15
- <div className="flex items-center justify-center w-24 h-24 bg-purple-600 rounded-full shadow-2xl animate-pulse cursor-pointer">
16
- <span className="text-white font-bold text-xl">AI</span>
17
- </div>
18
- );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
- // --- RENDER FUNCTIONS ---
21
 
22
- // 1. Configuration Phase
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  const renderConfig = () => (
24
  <div className="bg-white p-6 rounded-xl shadow-lg border border-gray-200">
25
  <h3 className="text-2xl font-semibold mb-6 flex items-center gap-2 text-blue-700">
26
  <Settings size={24} /> Configure Your Interview
27
  </h3>
28
-
29
  <div className="space-y-6">
30
- {/* Job Role Input */}
31
  <label className="block">
32
- <span className="text-gray-700 font-medium">1. Job Role/Position (e.g., Senior Frontend Developer)</span>
 
 
33
  <input
34
  type="text"
35
  value={jobRole}
36
  onChange={(e) => setJobRole(e.target.value)}
37
- placeholder="Enter the job role for tailored questions"
38
  className="mt-1 block w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500"
39
  />
40
  </label>
41
 
42
- {/* Experience Input */}
43
  <label className="block">
44
- <span className="text-gray-700 font-medium">2. Years of Professional Experience</span>
 
 
45
  <input
46
  type="number"
47
- min="0"
48
  value={experience}
49
  onChange={(e) => setExperience(e.target.value)}
50
  placeholder="e.g., 5"
@@ -52,7 +141,6 @@ const AIInterview: React.FC = () => {
52
  />
53
  </label>
54
 
55
- {/* Level Select */}
56
  <label className="block">
57
  <span className="text-gray-700 font-medium">3. Difficulty Level</span>
58
  <select
@@ -60,77 +148,85 @@ const AIInterview: React.FC = () => {
60
  onChange={(e) => setLevel(e.target.value)}
61
  className="mt-1 block w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500 bg-white"
62
  >
63
- <option value="Basic">Basic (Beginner)</option>
64
- <option value="Medium">Medium (Intermediate)</option>
65
- <option value="Hard">Hard (Senior/Expert)</option>
66
  </select>
67
  </label>
68
  </div>
69
 
70
  <button
71
- onClick={() => {
72
- if (jobRole && experience) {
73
- setInterviewState('chat');
74
- } else {
75
- alert('Please fill in the Job Role and Experience.');
76
- }
77
- }}
78
- className="mt-8 px-8 py-3 bg-blue-600 text-white font-semibold rounded-lg hover:bg-blue-700 transition duration-150 flex items-center gap-2"
79
- disabled={!jobRole || !experience}
80
  >
81
- <Send size={20} /> Start Interview
 
 
 
 
 
 
 
82
  </button>
83
  </div>
84
  );
85
 
86
- // 2. Chat/Interview Phase
87
  const renderChat = () => (
88
- <div className="flex flex-col items-center bg-white p-6 rounded-xl shadow-lg h-[600px] overflow-hidden relative">
89
- <h3 className="text-xl font-bold mb-4">Interview in Progress</h3>
90
- <p className="text-gray-600 mb-6">
91
- Role: {jobRole} | Experience: {experience} yrs | Level: {level}
92
  </p>
93
 
94
- {/* The AI Ball UI Element */}
95
- <InterviewBall />
96
-
97
- {/* Placeholder for Chat Messages */}
98
- <div className="flex-1 w-full mt-4 p-4 border border-dashed border-gray-300 rounded-lg overflow-y-auto bg-gray-50">
99
- <div className="bg-purple-100 p-3 rounded-lg text-purple-800 mb-2">
100
- **AI:** Welcome! Based on your configuration, let's start with your first question...
 
 
 
 
 
 
 
 
 
 
101
  </div>
102
- {/* User messages and AI responses would go here */}
103
- </div>
104
 
105
- {/* Input area */}
106
- <div className="w-full flex gap-2 mt-4">
107
- <input
108
- type="text"
109
- placeholder="Type your answer here..."
110
- className="flex-1 px-4 py-3 border border-gray-300 rounded-lg focus:ring-purple-500 focus:border-purple-500"
111
- />
112
- <button className="px-4 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700">
113
- <Send size={20} />
114
- </button>
115
  </div>
116
-
117
- <button
118
- onClick={() => setInterviewState('results')}
119
- className="mt-4 text-sm text-red-500 hover:text-red-700 underline"
120
  >
121
- End Interview
122
  </button>
123
  </div>
124
  );
125
-
126
- // 3. Results Phase (Simple Placeholder)
127
  const renderResults = () => (
128
  <div className="bg-green-50 p-8 rounded-xl shadow-xl text-center">
129
  <CheckCircle size={48} className="text-green-600 mx-auto mb-4" />
130
- <h3 className="text-3xl font-bold text-green-700 mb-2">Interview Ended</h3>
131
- <p className="text-gray-700 mb-6">Thank you for practicing! Your detailed feedback and results are being compiled now.</p>
132
- <button
133
- onClick={() => setInterviewState('config')}
 
 
 
 
 
134
  className="px-6 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 transition duration-150"
135
  >
136
  Start New Interview
@@ -138,18 +234,16 @@ const AIInterview: React.FC = () => {
138
  </div>
139
  );
140
 
141
- // --- MAIN RENDER ---
142
-
143
  return (
144
- <div className="p-8 max-w-4xl mx-auto">
145
- <h1 className="text-4xl font-extrabold text-gray-800 mb-6">AI Interview Practice 🤖</h1>
146
-
147
- {/* State rendering */}
148
- {interviewState === 'config' && renderConfig()}
149
- {interviewState === 'chat' && renderChat()}
150
- {interviewState === 'results' && renderResults()}
151
  </div>
152
  );
153
  };
154
 
155
- export default AIInterview;
 
1
+ import React, { useState, useEffect } from "react";
2
+ import {
3
+ Send,
4
+ Settings,
5
+ CheckCircle,
6
+ Mic,
7
+ PhoneOff,
8
+ Volume2,
9
+ Loader2,
10
+ Activity,
11
+ } from "lucide-react";
12
+ import Vapi from "@vapi-ai/web";
13
+ import API from "../api/api"; // Your Axios instance
14
+ import { useAuth } from "../components/context/AuthContext";
15
 
16
+ // --- CONFIG ---
17
+ const VAPI_PUBLIC_KEY = "6e393730-74a2-4690-8cb7-845ed3880488"; // Replace with your key
18
+ const vapi = new Vapi(VAPI_PUBLIC_KEY);
19
+
20
+ type InterviewState = "config" | "chat" | "results";
21
 
22
  const AIInterview: React.FC = () => {
23
+ const { username } = useAuth(); // Get logged in user name
24
+ const [interviewState, setInterviewState] =
25
+ useState<InterviewState>("config");
26
+
27
+ // Form State
28
+ const [jobRole, setJobRole] = useState("");
29
+ const [experience, setExperience] = useState("");
30
+ const [level, setLevel] = useState("Medium");
31
+
32
+ // Vapi State
33
+ const [isSessionActive, setIsSessionActive] = useState(false);
34
+ const [isSpeaking, setIsSpeaking] = useState(false);
35
+ const [status, setStatus] = useState("Idle");
36
+ const [volumeLevel, setVolumeLevel] = useState(0);
37
+
38
+ // --- VAPI EVENTS ---
39
+ useEffect(() => {
40
+ vapi.on("call-start", () => {
41
+ setStatus("Connected");
42
+ setIsSessionActive(true);
43
+ setInterviewState("chat"); // Switch UI to chat when call starts
44
+ });
45
+
46
+ vapi.on("call-end", () => {
47
+ setStatus("Call Ended");
48
+ setIsSessionActive(false);
49
+ setIsSpeaking(false);
50
+ setInterviewState("results"); // Switch UI to results when call ends
51
+ });
52
+
53
+ vapi.on("speech-start", () => setIsSpeaking(true));
54
+ vapi.on("speech-end", () => setIsSpeaking(false));
55
+
56
+ vapi.on("volume-level", (level) => setVolumeLevel(level)); // Optional: for animation
57
+
58
+ vapi.on("error", (e) => {
59
+ console.error("Vapi Error:", e);
60
+ setStatus("Error connecting");
61
+ setIsSessionActive(false);
62
+ });
63
+
64
+ // Cleanup
65
+ return () => {
66
+ vapi.stop();
67
+ vapi.removeAllListeners();
68
+ };
69
+ }, []);
70
+
71
+ // --- ACTIONS ---
72
+
73
+ const startInterview = async () => {
74
+ if (!jobRole || !experience) {
75
+ alert("Please fill in Job Role and Experience.");
76
+ return;
77
+ }
78
+
79
+ setStatus("Configuring AI...");
80
+
81
+ try {
82
+ // 1. Get dynamic config from YOUR backend
83
+ const response = await API.post("/vapi/get-vapi-config", {
84
+ name: username,
85
+ job_role: jobRole,
86
+ experience: experience,
87
+ level: level,
88
+ });
89
 
90
+ const { assistantId, overrides } = response.data;
91
 
92
+ setStatus("Connecting...");
93
+
94
+ // 2. Start Vapi Call
95
+ await vapi.start(assistantId, overrides);
96
+ } catch (err) {
97
+ console.error("Failed to start interview:", err);
98
+ setStatus("Failed to start");
99
+ alert("Could not start interview. Check backend connection.");
100
+ }
101
+ };
102
+
103
+ const endInterview = () => {
104
+ vapi.stop();
105
+ // State change to 'results' happens in 'call-end' listener
106
+ };
107
+
108
+ // --- RENDERERS ---
109
+
110
+ // 1. Configuration Phase (Kept mostly same as original)
111
  const renderConfig = () => (
112
  <div className="bg-white p-6 rounded-xl shadow-lg border border-gray-200">
113
  <h3 className="text-2xl font-semibold mb-6 flex items-center gap-2 text-blue-700">
114
  <Settings size={24} /> Configure Your Interview
115
  </h3>
116
+
117
  <div className="space-y-6">
 
118
  <label className="block">
119
+ <span className="text-gray-700 font-medium">
120
+ 1. Job Role/Position
121
+ </span>
122
  <input
123
  type="text"
124
  value={jobRole}
125
  onChange={(e) => setJobRole(e.target.value)}
126
+ placeholder="e.g., Senior Frontend Developer"
127
  className="mt-1 block w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500"
128
  />
129
  </label>
130
 
 
131
  <label className="block">
132
+ <span className="text-gray-700 font-medium">
133
+ 2. Years of Experience
134
+ </span>
135
  <input
136
  type="number"
 
137
  value={experience}
138
  onChange={(e) => setExperience(e.target.value)}
139
  placeholder="e.g., 5"
 
141
  />
142
  </label>
143
 
 
144
  <label className="block">
145
  <span className="text-gray-700 font-medium">3. Difficulty Level</span>
146
  <select
 
148
  onChange={(e) => setLevel(e.target.value)}
149
  className="mt-1 block w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500 bg-white"
150
  >
151
+ <option value="Basic">Basic</option>
152
+ <option value="Medium">Medium</option>
153
+ <option value="Hard">Hard</option>
154
  </select>
155
  </label>
156
  </div>
157
 
158
  <button
159
+ onClick={startInterview}
160
+ disabled={status === "Configuring AI..." || status === "Connecting..."}
161
+ className="mt-8 px-8 py-3 bg-blue-600 text-white font-semibold rounded-lg hover:bg-blue-700 transition duration-150 flex items-center gap-2 disabled:opacity-70"
 
 
 
 
 
 
162
  >
163
+ {status === "Configuring AI..." || status === "Connecting..." ? (
164
+ <Loader2 className="animate-spin" />
165
+ ) : (
166
+ <Mic size={20} />
167
+ )}
168
+ {status === "Idle" || status === "Error"
169
+ ? "Start Voice Interview"
170
+ : status}
171
  </button>
172
  </div>
173
  );
174
 
175
+ // 2. Active Chat Phase (Modified for Voice UI)
176
  const renderChat = () => (
177
+ <div className="flex flex-col items-center justify-center bg-white p-8 rounded-xl shadow-lg min-h-[500px] relative">
178
+ <h3 className="text-xl font-bold mb-2">Live Interview</h3>
179
+ <p className="text-gray-600 mb-8">
180
+ {jobRole} {level} Level
181
  </p>
182
 
183
+ {/* Dynamic AI Ball based on Speaking State */}
184
+ <div className="relative mb-10">
185
+ <div
186
+ className={`flex items-center justify-center w-32 h-32 rounded-full shadow-2xl transition-all duration-300 ${
187
+ isSpeaking ? "bg-purple-600 scale-110" : "bg-blue-600 scale-100"
188
+ }`}
189
+ style={{
190
+ boxShadow: isSpeaking
191
+ ? `0 0 ${30 + volumeLevel * 50}px rgba(147, 51, 234, 0.6)`
192
+ : "0 0 20px rgba(37, 99, 235, 0.3)",
193
+ }}
194
+ >
195
+ {isSpeaking ? (
196
+ <Volume2 className="w-12 h-12 text-white animate-pulse" />
197
+ ) : (
198
+ <Activity className="w-12 h-12 text-white" />
199
+ )}
200
  </div>
 
 
201
 
202
+ {/* Status Indicator */}
203
+ <div className="absolute -bottom-10 left-1/2 -translate-x-1/2 whitespace-nowrap text-gray-500 font-medium animate-pulse">
204
+ {isSpeaking ? "AI is speaking..." : "Listening to you..."}
205
+ </div>
 
 
 
 
 
 
206
  </div>
207
+
208
+ <button
209
+ onClick={endInterview}
210
+ className="mt-8 flex items-center gap-2 px-6 py-3 bg-red-100 text-red-600 rounded-full hover:bg-red-200 transition font-semibold"
211
  >
212
+ <PhoneOff size={20} /> End Interview
213
  </button>
214
  </div>
215
  );
216
+
217
+ // 3. Results Phase
218
  const renderResults = () => (
219
  <div className="bg-green-50 p-8 rounded-xl shadow-xl text-center">
220
  <CheckCircle size={48} className="text-green-600 mx-auto mb-4" />
221
+ <h3 className="text-3xl font-bold text-green-700 mb-2">
222
+ Interview Completed
223
+ </h3>
224
+ <p className="text-gray-700 mb-6">
225
+ The AI interviewer has finished assessing your responses. (Integration
226
+ with transcript analysis would go here).
227
+ </p>
228
+ <button
229
+ onClick={() => setInterviewState("config")}
230
  className="px-6 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 transition duration-150"
231
  >
232
  Start New Interview
 
234
  </div>
235
  );
236
 
 
 
237
  return (
238
+ <div className="p-8 max-w-4xl mx-auto font-opensans">
239
+ <h1 className="text-4xl font-extrabold text-gray-800 mb-6">
240
+ AI Voice Interview 🎙️
241
+ </h1>
242
+ {interviewState === "config" && renderConfig()}
243
+ {interviewState === "chat" && renderChat()}
244
+ {interviewState === "results" && renderResults()}
245
  </div>
246
  );
247
  };
248
 
249
+ export default AIInterview;