aki-008
commited on
Commit
·
893ff0d
1
Parent(s):
3a72516
feat: AI interview setup
Browse files- Backend/app/api/v1/api.py +7 -1
- Backend/app/api/v1/endpoints/notes.py +1 -1
- Backend/app/api/v1/endpoints/prompts.py +10 -0
- Backend/app/api/v1/endpoints/vapi_ai.py +36 -0
- Backend/app/config.py +3 -0
- Backend/app/schema/__init__.py +2 -2
- Backend/app/schema/models.py +8 -1
- Backend/requirements.txt +2 -1
- Frontend/src/pages/AiInterview.tsx +177 -83
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 {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
|
| 4 |
-
//
|
| 5 |
-
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
const AIInterview: React.FC = () => {
|
| 8 |
-
const
|
| 9 |
-
const [
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
const
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
-
|
| 21 |
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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">
|
|
|
|
|
|
|
| 33 |
<input
|
| 34 |
type="text"
|
| 35 |
value={jobRole}
|
| 36 |
onChange={(e) => setJobRole(e.target.value)}
|
| 37 |
-
placeholder="
|
| 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">
|
|
|
|
|
|
|
| 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
|
| 64 |
-
<option value="Medium">Medium
|
| 65 |
-
<option value="Hard">Hard
|
| 66 |
</select>
|
| 67 |
</label>
|
| 68 |
</div>
|
| 69 |
|
| 70 |
<button
|
| 71 |
-
onClick={
|
| 72 |
-
|
| 73 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
</button>
|
| 83 |
</div>
|
| 84 |
);
|
| 85 |
|
| 86 |
-
// 2. Chat
|
| 87 |
const renderChat = () => (
|
| 88 |
-
<div className="flex flex-col items-center bg-white p-
|
| 89 |
-
<h3 className="text-xl font-bold mb-
|
| 90 |
-
<p className="text-gray-600 mb-
|
| 91 |
-
|
| 92 |
</p>
|
| 93 |
|
| 94 |
-
{/*
|
| 95 |
-
<
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
</div>
|
| 102 |
-
{/* User messages and AI responses would go here */}
|
| 103 |
-
</div>
|
| 104 |
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 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={
|
| 119 |
-
className="mt-
|
| 120 |
>
|
| 121 |
-
End Interview
|
| 122 |
</button>
|
| 123 |
</div>
|
| 124 |
);
|
| 125 |
-
|
| 126 |
-
// 3. Results Phase
|
| 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">
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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">
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
{interviewState ===
|
| 149 |
-
{interviewState ===
|
| 150 |
-
{interviewState ===
|
| 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;
|