aki-008 commited on
Commit
6c6eb30
·
1 Parent(s): 6e961ea

feat: updated frontend for ai interview

Browse files
Backend/app/api/v1/endpoints/interview.py CHANGED
@@ -26,22 +26,13 @@ except Exception as e:
26
  print(f"Vapi SDK Initialization Error: {e}")
27
  print("Ensure VAPI_PRIVATE_KEY is set in .env")
28
 
29
- # --- CORS SETUP ---
30
- # app.add_middleware(
31
- # CORSMiddleware,
32
- # # Allow communication from the frontend running on localhost:5173
33
- # # and also allow the ngrok base URL for safety.
34
- # allow_origins=["http://localhost:5173", "http://127.0.0.1:5173", SERVER_URL],
35
- # allow_credentials=True,
36
- # allow_methods=["*"],
37
- # allow_headers=["*"],
38
- # )
39
 
40
  # --- SCHEMAS ---
41
  class ConfigRequest(BaseModel):
42
  name: str
43
  job_role: str
44
  experience: str
 
45
 
46
  # --- ENDPOINTS ---
47
 
@@ -58,19 +49,17 @@ async def get_vapi_config(data: ConfigRequest):
58
 
59
  try:
60
  print(f"\n--- New Interview Request ---")
61
- print(f"👤 User: {data.name}, Role: {data.job_role}, Exp: {data.experience}")
62
 
63
- # 1. Construct the Dynamic System Prompt
64
  system_prompt = (
65
  f"You are a strict technical interviewer. You are interviewing {data.name} for a {data.job_role} role. "
66
  f"They have {data.experience} years of experience. "
 
 
 
67
  f"Ask short, concise questions. Wait for their answer. Do not lecture. "
68
- f"Start by asking them to introduce themselves and briefly describe their experience."
69
  )
70
-
71
- # 2. Dynamic Webhook URL (for this call only)
72
- # Vapi will send webhooks to the public URL defined in SERVER_URL,
73
- # specifically hitting this backend's /api/webhook route.
74
  webhook_url = f"{SERVER_URL}/api/webhook"
75
 
76
  # 3. Construct the Overrides Payload
 
26
  print(f"Vapi SDK Initialization Error: {e}")
27
  print("Ensure VAPI_PRIVATE_KEY is set in .env")
28
 
 
 
 
 
 
 
 
 
 
 
29
 
30
  # --- SCHEMAS ---
31
  class ConfigRequest(BaseModel):
32
  name: str
33
  job_role: str
34
  experience: str
35
+ level: str = "Medium"
36
 
37
  # --- ENDPOINTS ---
38
 
 
49
 
50
  try:
51
  print(f"\n--- New Interview Request ---")
52
+ print(f"👤 User: {data.name}, Role: {data.job_role}, Exp: {data.experience}, Level: {data.level}")
53
 
 
54
  system_prompt = (
55
  f"You are a strict technical interviewer. You are interviewing {data.name} for a {data.job_role} role. "
56
  f"They have {data.experience} years of experience. "
57
+ f"The interview difficulty level is {data.level}. "
58
+ f"If the level is 'Hard', ask complex, multi-layered questions. "
59
+ f"If 'Medium', focus on standard industry concepts. "
60
  f"Ask short, concise questions. Wait for their answer. Do not lecture. "
61
+ f"Start by asking them to introduce themselves."
62
  )
 
 
 
 
63
  webhook_url = f"{SERVER_URL}/api/webhook"
64
 
65
  # 3. Construct the Overrides Payload
Frontend/index.html CHANGED
@@ -8,8 +8,8 @@
8
  <title>Prep-AI</title>
9
  <link rel="preconnect" href="https://fonts.googleapis.com">
10
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
- <link href="https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300..800;1,300..800&display=swap"
12
- rel="stylesheet">
13
  </head>
14
 
15
  <body>
 
8
  <title>Prep-AI</title>
9
  <link rel="preconnect" href="https://fonts.googleapis.com">
10
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
+ <link href="https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300..800;1,300..800&display=swap" rel="stylesheet">
12
+ <link href="https://fonts.googleapis.com/css2?family=Patrick+Hand&display=swap" rel="stylesheet">
13
  </head>
14
 
15
  <body>
Frontend/src/components/dashboard/Sidebar.tsx CHANGED
@@ -1,6 +1,16 @@
1
  import React, { useState } from "react";
2
  import { Link, useLocation } from "react-router-dom";
3
- import { Home, FileText, Brain, BookOpen, Settings, User, LogOut, ChevronLeft, ChevronRight } from "lucide-react";
 
 
 
 
 
 
 
 
 
 
4
  import { useAuth } from "../context/AuthContext";
5
 
6
  const Sidebar: React.FC = () => {
@@ -9,31 +19,45 @@ const Sidebar: React.FC = () => {
9
  const [collapsed, setCollapsed] = useState(false);
10
 
11
  const navItems = [
12
- { path: "/dashboard", label: "Dashboard", icon: <Home size={18} /> },
13
- { path: "/notes", label: "Notes", icon: <BookOpen size={18} /> },
14
- { path: "/AIInterview", label: "AI Interview", icon: <Brain size={18} /> },
15
- { path: "/quize", label: "Resume Quiz", icon: <FileText size={18} /> },
16
  ];
17
 
18
  return (
19
  <aside
20
- className={`flex flex-col justify-between h-screen bg-linear-to-r from-blue-700 to-gray-900/50 text-white transition-all duration-300 ${collapsed ? "w-20" : "w-64"}`}
 
 
21
  >
22
  {/* Top Section */}
23
  <div>
24
- <div className="flex items-center justify-between text-2xl font-bold py-6 px-4 border-b border-gray-700">
25
- {!collapsed && <span>InterviewAI</span>}
26
- <button onClick={() => setCollapsed(!collapsed)} className="p-1">
27
- {collapsed ? <ChevronRight size={18} /> : <ChevronLeft size={18} />}
 
 
 
 
 
 
 
28
  </button>
29
  </div>
30
 
31
- <nav className="p-4 space-y-3">
32
  {navItems.map((item) => (
33
  <Link
34
  key={item.path}
35
  to={item.path}
36
- className={`flex items-center space-x-3 p-3 rounded-lg hover:bg-gray-800 transition ${location.pathname === item.path ? "bg-gray-800" : ""}`}
 
 
 
 
 
37
  >
38
  {item.icon}
39
  {!collapsed && <span>{item.label}</span>}
@@ -43,22 +67,22 @@ const Sidebar: React.FC = () => {
43
  </div>
44
 
45
  {/* Bottom Section */}
46
- <div className="p-4 border-t border-gray-700 space-y-4">
47
- <button className="flex items-center space-x-3 p-3 rounded-lg hover:bg-gray-800 transition w-full text-left text-gray-300">
48
- <Settings size={18} />
49
  {!collapsed && <span>Settings</span>}
50
  </button>
51
 
52
- <button className="flex items-center space-x-3 p-3 rounded-lg hover:bg-gray-800 transition w-full text-left text-gray-300">
53
- <User size={18} />
54
  {!collapsed && <span>{username}</span>}
55
  </button>
56
 
57
  <button
58
  onClick={logout}
59
- className="flex items-center space-x-3 p-3 rounded-lg bg-red-500 hover:bg-red-600 text-black hover:text-white transition w-full text-left font-medium"
60
  >
61
- <LogOut size={18} />
62
  {!collapsed && <span>Logout</span>}
63
  </button>
64
  </div>
@@ -66,4 +90,4 @@ const Sidebar: React.FC = () => {
66
  );
67
  };
68
 
69
- export default Sidebar;
 
1
  import React, { useState } from "react";
2
  import { Link, useLocation } from "react-router-dom";
3
+ import {
4
+ Home,
5
+ FileText,
6
+ Brain,
7
+ BookOpen,
8
+ Settings,
9
+ User,
10
+ LogOut,
11
+ ChevronLeft,
12
+ ChevronRight,
13
+ } from "lucide-react";
14
  import { useAuth } from "../context/AuthContext";
15
 
16
  const Sidebar: React.FC = () => {
 
19
  const [collapsed, setCollapsed] = useState(false);
20
 
21
  const navItems = [
22
+ { path: "/dashboard", label: "Dashboard", icon: <Home size={20} /> },
23
+ { path: "/notes", label: "Notes", icon: <BookOpen size={20} /> },
24
+ { path: "/AIInterview", label: "AI Interview", icon: <Brain size={20} /> },
25
+ { path: "/quize", label: "Resume Quiz", icon: <FileText size={20} /> },
26
  ];
27
 
28
  return (
29
  <aside
30
+ className={`flex flex-col justify-between h-screen bg-maya-dark text-white transition-all duration-300 shadow-xl border-r border-indigo-400/30 ${
31
+ collapsed ? "w-20" : "w-64"
32
+ }`}
33
  >
34
  {/* Top Section */}
35
  <div>
36
+ <div className="flex items-center justify-between font-bold py-6 px-6 border-b border-indigo-400/30">
37
+ {!collapsed && (
38
+ <span className="text-xl tracking-wide font-handwriting">
39
+ Maya AI
40
+ </span>
41
+ )}
42
+ <button
43
+ onClick={() => setCollapsed(!collapsed)}
44
+ className="p-1 hover:bg-white/10 rounded-full transition"
45
+ >
46
+ {collapsed ? <ChevronRight size={20} /> : <ChevronLeft size={20} />}
47
  </button>
48
  </div>
49
 
50
+ <nav className="p-4 space-y-2">
51
  {navItems.map((item) => (
52
  <Link
53
  key={item.path}
54
  to={item.path}
55
+ className={`flex items-center space-x-3 p-3 rounded-xl transition-all duration-200
56
+ ${
57
+ location.pathname === item.path
58
+ ? "bg-white text-maya-dark shadow-md font-semibold translate-x-1"
59
+ : "hover:bg-white/10 text-indigo-50"
60
+ }`}
61
  >
62
  {item.icon}
63
  {!collapsed && <span>{item.label}</span>}
 
67
  </div>
68
 
69
  {/* Bottom Section */}
70
+ <div className="p-4 border-t border-indigo-400/30 space-y-2">
71
+ <button className="flex items-center space-x-3 p-3 rounded-xl hover:bg-white/10 transition w-full text-left text-indigo-100">
72
+ <Settings size={20} />
73
  {!collapsed && <span>Settings</span>}
74
  </button>
75
 
76
+ <button className="flex items-center space-x-3 p-3 rounded-xl hover:bg-white/10 transition w-full text-left text-indigo-100">
77
+ <User size={20} />
78
  {!collapsed && <span>{username}</span>}
79
  </button>
80
 
81
  <button
82
  onClick={logout}
83
+ className="flex items-center space-x-3 p-3 rounded-xl bg-red-400/20 hover:bg-red-400/40 text-red-100 hover:text-white transition w-full text-left"
84
  >
85
+ <LogOut size={20} />
86
  {!collapsed && <span>Logout</span>}
87
  </button>
88
  </div>
 
90
  );
91
  };
92
 
93
+ export default Sidebar;
Frontend/src/pages/AiInterview.tsx CHANGED
@@ -1,97 +1,85 @@
1
  import React, { useEffect, useState } from "react";
2
  import Vapi from "@vapi-ai/web";
3
- import { Mic, PhoneOff, Volume2, Loader2, Activity } from "lucide-react";
4
 
5
  // --- CONFIG ---
6
- // 1. Put your Vapi Public Key here
7
- const VAPI_PUBLIC_KEY = "6e393730-74a2-4690-8cb7-845ed3880488";
8
- // 2. Point this to your FastAPI backend
9
  const BACKEND_URL = "http://localhost:8000";
10
 
11
  const vapi = new Vapi(VAPI_PUBLIC_KEY);
12
 
13
- function App() {
 
 
 
 
14
  const [isSessionActive, setIsSessionActive] = useState(false);
15
  const [isSpeaking, setIsSpeaking] = useState(false);
16
  const [status, setStatus] = useState("Idle");
17
 
18
  // Form State
19
- const [name, setName] = useState("Jane Doe");
20
- const [role, setRole] = useState("Senior Frontend Engineer");
21
- const [exp, setExp] = useState("7");
 
22
 
23
  useEffect(() => {
24
  // Vapi Event Listeners
25
  vapi.on("call-start", () => {
26
- setStatus("Connected (AI is listening)");
27
  setIsSessionActive(true);
 
28
  });
29
 
30
  vapi.on("call-end", () => {
31
  setStatus("Call Ended");
32
  setIsSessionActive(false);
33
  setIsSpeaking(false);
 
34
  });
35
 
36
- vapi.on("speech-start", () => {
37
- setStatus("AI is speaking...");
38
- setIsSpeaking(true);
39
- });
40
-
41
- vapi.on("speech-end", () => {
42
- if (isSessionActive) {
43
- setStatus("Connected (Listening)");
44
- setIsSpeaking(false);
45
- }
46
- });
47
 
48
  vapi.on("error", (e) => {
49
  console.error("Vapi Error:", e);
50
- setStatus(`Error: ${e.message}`);
51
  setIsSessionActive(false);
 
52
  });
53
 
54
  return () => {
55
- // Cleanup
56
  vapi.stop();
57
  vapi.removeAllListeners();
58
  };
59
  }, []);
60
 
61
  const startInterview = async () => {
62
- if (VAPI_PUBLIC_KEY === "YOUR_PUBLIC_KEY_HERE") {
63
- alert("Please update VAPI_PUBLIC_KEY in src/App.tsx");
64
- return;
65
- }
66
-
67
  setStatus("Configuring...");
68
-
69
  try {
70
- // 1. Call your backend to get the dynamic config
71
  const response = await fetch(
72
  `${BACKEND_URL}/api/v1/interview/api/get-vapi-config`,
73
  {
74
  method: "POST",
75
  headers: { "Content-Type": "application/json" },
76
- body: JSON.stringify({ name, job_role: role, experience: exp }),
 
 
 
 
 
77
  }
78
  );
79
 
80
  const data = await response.json();
81
-
82
- if (!data.assistantId || !data.overrides) {
83
- throw new Error("Invalid config from backend");
84
- }
85
 
86
  setStatus("Connecting...");
87
-
88
- // 2. Start Vapi with the config from backend
89
  vapi.start(data.assistantId, data.overrides);
90
  } catch (err: any) {
91
- console.error("Start Call API Error:", err);
92
- setStatus(
93
- `Failed: ${err.message || "Check FastAPI terminal for details."}`
94
- );
95
  }
96
  };
97
 
@@ -99,177 +87,173 @@ function App() {
99
  vapi.stop();
100
  };
101
 
 
102
  return (
103
- <div
104
- style={{
105
- backgroundColor: "#1f2937" /* Slate 800 */,
106
- padding: "2.5rem",
107
- borderRadius: "1rem",
108
- boxShadow: "0 20px 25px -5px rgba(0, 0, 0, 0.5)",
109
- maxWidth: "450px",
110
- width: "100%",
111
- textAlign: "center",
112
- color: "#f9fafb" /* Gray 50 */,
113
- }}
114
- >
115
- <h1
116
- style={{
117
- fontSize: "2rem",
118
- fontWeight: "bold",
119
- marginBottom: "1.5rem",
120
- color: "#60a5fa",
121
- }}
122
- >
123
- Vapi Interview Tester
124
- </h1>
125
 
126
- {/* --- FORM --- */}
127
- {!isSessionActive && (
128
- <div
129
- style={{
130
- display: "flex",
131
- flexDirection: "column",
132
- gap: "1rem",
133
- marginBottom: "1.5rem",
134
- }}
135
- >
136
- <input
137
- type="text"
138
- placeholder="Your Name"
139
- value={name}
140
- onChange={(e) => setName(e.target.value)}
141
- style={inputStyle}
142
- />
143
- <input
144
- type="text"
145
- placeholder="Target Role"
146
- value={role}
147
- onChange={(e) => setRole(e.target.value)}
148
- style={inputStyle}
149
- />
150
- <input
151
- type="number"
152
- placeholder="Experience (Years)"
153
- value={exp}
154
- onChange={(e) => setExp(e.target.value)}
155
- style={inputStyle}
156
- />
157
- </div>
158
- )}
159
 
160
- {/* --- VISUALIZER --- */}
161
- <div
162
- style={{ margin: "2rem 0", display: "flex", justifyContent: "center" }}
163
- >
164
- <div
165
- style={{
166
- width: "120px",
167
- height: "120px",
168
- borderRadius: "50%",
169
- backgroundColor: isSpeaking
170
- ? "#a855f7"
171
- : isSessionActive
172
- ? "#2563eb"
173
- : "#4b5563",
174
- display: "flex",
175
- alignItems: "center",
176
- justifyContent: "center",
177
- color: "white",
178
- transition: "all 0.3s ease",
179
- transform: isSpeaking ? "scale(1.1)" : "scale(1)",
180
- boxShadow: isSpeaking
181
- ? "0 0 30px rgba(168, 85, 247, 0.8)"
182
- : "0 0 20px rgba(37, 99, 235, 0.4)",
183
- }}
184
- >
185
- {!isSessionActive ? (
186
- <Mic size={48} />
187
- ) : isSpeaking ? (
188
- <Volume2 size={48} />
189
- ) : (
190
- <Activity size={48} />
191
- )}
192
- </div>
193
- </div>
194
 
195
- {/* --- CONTROLS --- */}
196
- <div style={{ display: "flex", justifyContent: "center", gap: "1rem" }}>
197
- {!isSessionActive ? (
198
- <button
199
- onClick={startInterview}
200
- style={btnPrimary}
201
- disabled={status === "Connecting..." || status === "Configuring..."}
202
- >
203
- {status === "Connecting..." || status === "Configuring..." ? (
204
- <Loader2 className="animate-spin" />
205
- ) : (
206
- <Mic />
207
- )}
208
- {status === "Connecting..." || status === "Configuring..."
209
- ? "Starting..."
210
- : "Start Interview"}
211
- </button>
212
- ) : (
213
- <button onClick={stopInterview} style={btnDestructive}>
214
- <PhoneOff /> End Call
215
- </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
  )}
217
- </div>
218
 
219
- <p
220
- style={{ marginTop: "1.5rem", color: "#9ca3af", fontSize: "0.875rem" }}
221
- >
222
- Status:{" "}
223
- <strong
224
- style={{ color: status.includes("Error") ? "#f87171" : "#e5e7eb" }}
225
- >
226
- {status}
227
- </strong>
228
- </p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229
  </div>
230
  );
231
  }
232
 
233
- // Simple inline styles for standalone testing
234
- const inputStyle = {
235
- padding: "0.75rem",
236
- borderRadius: "0.5rem",
237
- border: "1px solid #475569" /* Slate 600 */,
238
- fontSize: "1rem",
239
- backgroundColor: "#0f172a" /* Slate 900 */,
240
- color: "#f9fafb" /* Gray 50 */,
241
- };
242
-
243
- const btnPrimary = {
244
- display: "flex",
245
- alignItems: "center",
246
- gap: "0.5rem",
247
- backgroundColor: "#2563eb" /* Blue 600 */,
248
- color: "white",
249
- padding: "0.75rem 1.5rem",
250
- borderRadius: "0.5rem",
251
- border: "none",
252
- cursor: "pointer",
253
- fontSize: "1rem",
254
- fontWeight: "bold",
255
- transition: "background-color 0.2s",
256
- outline: "none",
257
- };
258
-
259
- const btnDestructive = {
260
- display: "flex",
261
- alignItems: "center",
262
- gap: "0.5rem",
263
- backgroundColor: "#dc2626" /* Red 600 */,
264
- color: "white",
265
- padding: "0.75rem 1.5rem",
266
- borderRadius: "0.5rem",
267
- border: "none",
268
- cursor: "pointer",
269
- fontSize: "1rem",
270
- fontWeight: "bold",
271
- transition: "background-color 0.2s",
272
- outline: "none",
273
- };
274
 
275
- export default App;
 
1
  import React, { useEffect, useState } from "react";
2
  import Vapi from "@vapi-ai/web";
3
+ import { Mic, PhoneOff, Send, Loader2, Volume2 } from "lucide-react";
4
 
5
  // --- CONFIG ---
6
+ const VAPI_PUBLIC_KEY = "6e393730-74a2-4690-8cb7-845ed3880488"; // Replace with yours
 
 
7
  const BACKEND_URL = "http://localhost:8000";
8
 
9
  const vapi = new Vapi(VAPI_PUBLIC_KEY);
10
 
11
+ function AIInterview() {
12
+ // View State
13
+ const [viewState, setViewState] = useState<"config" | "interview">("config");
14
+
15
+ // Vapi State
16
  const [isSessionActive, setIsSessionActive] = useState(false);
17
  const [isSpeaking, setIsSpeaking] = useState(false);
18
  const [status, setStatus] = useState("Idle");
19
 
20
  // Form State
21
+ const [name, setName] = useState("Prakhar"); // Default from screenshot
22
+ const [role, setRole] = useState("Senior Frontend Developer");
23
+ const [exp, setExp] = useState("5");
24
+ const [difficulty, setDifficulty] = useState("Medium (Intermediate)");
25
 
26
  useEffect(() => {
27
  // Vapi Event Listeners
28
  vapi.on("call-start", () => {
29
+ setStatus("Connected");
30
  setIsSessionActive(true);
31
+ setViewState("interview"); // Switch to bubble view
32
  });
33
 
34
  vapi.on("call-end", () => {
35
  setStatus("Call Ended");
36
  setIsSessionActive(false);
37
  setIsSpeaking(false);
38
+ setViewState("config"); // Go back to config
39
  });
40
 
41
+ vapi.on("speech-start", () => setIsSpeaking(true));
42
+ vapi.on("speech-end", () => setIsSpeaking(false));
 
 
 
 
 
 
 
 
 
43
 
44
  vapi.on("error", (e) => {
45
  console.error("Vapi Error:", e);
46
+ setStatus("Error connecting");
47
  setIsSessionActive(false);
48
+ setViewState("config");
49
  });
50
 
51
  return () => {
 
52
  vapi.stop();
53
  vapi.removeAllListeners();
54
  };
55
  }, []);
56
 
57
  const startInterview = async () => {
 
 
 
 
 
58
  setStatus("Configuring...");
 
59
  try {
 
60
  const response = await fetch(
61
  `${BACKEND_URL}/api/v1/interview/api/get-vapi-config`,
62
  {
63
  method: "POST",
64
  headers: { "Content-Type": "application/json" },
65
+ body: JSON.stringify({
66
+ name,
67
+ job_role: role,
68
+ experience: exp,
69
+ level: difficulty,
70
+ }),
71
  }
72
  );
73
 
74
  const data = await response.json();
75
+ if (!data.assistantId || !data.overrides)
76
+ throw new Error("Invalid config");
 
 
77
 
78
  setStatus("Connecting...");
 
 
79
  vapi.start(data.assistantId, data.overrides);
80
  } catch (err: any) {
81
+ console.error("Error:", err);
82
+ setStatus("Failed to start");
 
 
83
  }
84
  };
85
 
 
87
  vapi.stop();
88
  };
89
 
90
+ // --- RENDER ---
91
  return (
92
+ <div className="min-h-screen w-full font-sans text-gray-800 relative bg-white">
93
+ {/* 1. SKETCHY GRID BACKGROUND */}
94
+ <div className="absolute inset-0 z-0 bg-sketchy-grid bg-sketchy opacity-40 pointer-events-none"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
 
96
+ {/* 2. COLOR BLOBS (Palette: #F5D3C4, #F2AEBB) */}
97
+ <div className="absolute top-[-10%] right-[-5%] w-96 h-96 bg-maya-pink rounded-full mix-blend-multiply filter blur-3xl opacity-30 animate-blob"></div>
98
+ <div className="absolute bottom-[-10%] left-[-10%] w-96 h-96 bg-maya-beige rounded-full mix-blend-multiply filter blur-3xl opacity-30 animate-blob animation-delay-2000"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
 
100
+ {/* MAIN CONTENT AREA */}
101
+ <div className="relative z-10 flex flex-col items-center justify-center min-h-screen p-6">
102
+ {/* --- VIEW 1: CONFIG CARD (The "Maya" UI) --- */}
103
+ {viewState === "config" && (
104
+ <div className="w-full max-w-2xl bg-white rounded-xl shadow-2xl border border-gray-200 p-8 md:p-12 transition-all transform hover:scale-[1.005]">
105
+ <div className="mb-8 text-center">
106
+ <h1 className="text-4xl font-bold text-gray-800 mb-2 font-handwriting">
107
+ Maya
108
+ </h1>
109
+ <div className="flex items-center justify-center gap-2 text-maya-dark">
110
+ <BrainIcon />
111
+ <span className="font-semibold">Configure Your Interview</span>
112
+ </div>
113
+ </div>
114
+
115
+ <div className="space-y-6">
116
+ {/* Input 1: Role */}
117
+ <div>
118
+ <label className="block text-sm font-semibold text-gray-600 mb-2">
119
+ 1. Job Role/Position
120
+ </label>
121
+ <input
122
+ type="text"
123
+ value={role}
124
+ onChange={(e) => setRole(e.target.value)}
125
+ className="w-full p-4 rounded-lg border-2 border-gray-200 focus:border-maya-light focus:ring-0 outline-none transition bg-gray-50 text-gray-700 font-medium placeholder-gray-400"
126
+ placeholder="e.g. Senior Frontend Developer"
127
+ />
128
+ </div>
 
 
 
 
 
129
 
130
+ {/* Input 2: Experience */}
131
+ <div>
132
+ <label className="block text-sm font-semibold text-gray-600 mb-2">
133
+ 2. Years of Professional Experience
134
+ </label>
135
+ <input
136
+ type="text" // using text to match wireframe style, backend handles parsing
137
+ value={exp}
138
+ onChange={(e) => setExp(e.target.value)}
139
+ className="w-full p-4 rounded-lg border-2 border-gray-200 focus:border-maya-light focus:ring-0 outline-none transition bg-gray-50 text-gray-700 font-medium placeholder-gray-400"
140
+ placeholder="e.g. 5"
141
+ />
142
+ </div>
143
+
144
+ {/* Input 3: Difficulty */}
145
+ <div>
146
+ <label className="block text-sm font-semibold text-gray-600 mb-2">
147
+ 3. Difficulty Level
148
+ </label>
149
+ <input
150
+ type="text"
151
+ value={difficulty}
152
+ onChange={(e) => setDifficulty(e.target.value)}
153
+ className="w-full p-4 rounded-lg border-2 border-gray-200 focus:border-maya-light focus:ring-0 outline-none transition bg-gray-50 text-gray-700 font-medium placeholder-gray-400"
154
+ placeholder="Medium (Intermediate)"
155
+ />
156
+ </div>
157
+
158
+ {/* Action Button */}
159
+ <button
160
+ onClick={startInterview}
161
+ disabled={
162
+ status === "Configuring..." || status === "Connecting..."
163
+ }
164
+ className="mt-4 px-8 py-4 bg-white border-2 border-gray-800 text-gray-800 font-bold rounded-lg shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] hover:shadow-[2px_2px_0px_0px_rgba(0,0,0,1)] hover:translate-x-[2px] hover:translate-y-[2px] transition-all flex items-center gap-3 active:bg-gray-50"
165
+ >
166
+ {status === "Configuring..." || status === "Connecting..." ? (
167
+ <Loader2 className="animate-spin w-5 h-5" />
168
+ ) : (
169
+ <Send className="w-5 h-5" />
170
+ )}
171
+ <span>Start Interview</span>
172
+ </button>
173
+ </div>
174
+
175
+ {/* Decorative Color Codes (from wireframe) */}
176
+ <div className="absolute bottom-4 right-6 flex flex-col items-end text-xs text-gray-400 font-mono">
177
+ <span>#A7AAE1</span>
178
+ <span>#F5D3C4</span>
179
+ </div>
180
+ </div>
181
  )}
 
182
 
183
+ {/* --- VIEW 2: INTERVIEW BUBBLE UI (Active Call) --- */}
184
+ {viewState === "interview" && (
185
+ <div className="flex flex-col items-center justify-center w-full h-full animate-fade-in-up">
186
+ {/* The Visualizer Orb */}
187
+ <div className="relative mb-12">
188
+ {/* Orb Style: Light Blue/White Swirling
189
+ We use CSS gradients + shadowing + pulsing animation
190
+ */}
191
+ <div
192
+ className={`
193
+ w-48 h-48 rounded-full
194
+ bg-gradient-radial from-white via-blue-100 to-blue-300
195
+ shadow-[0_0_60px_rgba(167,170,225,0.6)]
196
+ transition-all duration-300 ease-in-out
197
+ flex items-center justify-center
198
+ border border-white/50
199
+ ${
200
+ isSpeaking
201
+ ? "scale-110 animate-pulse shadow-[0_0_80px_rgba(105,111,199,0.8)]"
202
+ : "scale-100 animate-orb-float"
203
+ }
204
+ `}
205
+ >
206
+ {/* Inner shine for "liquid" look */}
207
+ <div className="absolute top-4 left-6 w-12 h-6 bg-white opacity-60 rounded-full blur-md transform -rotate-45"></div>
208
+
209
+ {isSpeaking ? (
210
+ <Volume2 className="text-maya-dark w-12 h-12 opacity-50" />
211
+ ) : (
212
+ <div className="w-3 h-3 bg-maya-dark rounded-full opacity-30"></div>
213
+ )}
214
+ </div>
215
+ </div>
216
+
217
+ {/* Status Text */}
218
+ <h2 className="text-2xl font-bold text-gray-700 mb-8 font-handwriting">
219
+ {isSpeaking ? "Maya is speaking..." : "Listening to you..."}
220
+ </h2>
221
+
222
+ {/* Controls */}
223
+ <div className="flex gap-6">
224
+ <button className="flex items-center gap-2 px-6 py-3 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-full transition font-medium shadow-sm">
225
+ <Mic size={18} /> Mute
226
+ </button>
227
+
228
+ <button
229
+ onClick={stopInterview}
230
+ className="flex items-center gap-2 px-6 py-3 bg-red-100 hover:bg-red-200 text-red-600 rounded-full transition font-medium shadow-sm"
231
+ >
232
+ <PhoneOff size={18} /> End call
233
+ </button>
234
+ </div>
235
+ </div>
236
+ )}
237
+ </div>
238
  </div>
239
  );
240
  }
241
 
242
+ // Simple Icon Component
243
+ const BrainIcon = () => (
244
+ <svg
245
+ width="24"
246
+ height="24"
247
+ viewBox="0 0 24 24"
248
+ fill="none"
249
+ stroke="currentColor"
250
+ strokeWidth="2"
251
+ strokeLinecap="round"
252
+ strokeLinejoin="round"
253
+ >
254
+ <path d="M9.5 2A2.5 2.5 0 0 1 12 4.5v15a2.5 2.5 0 0 1-4.96.44 2.5 2.5 0 0 1-2.96-3.08 3 3 0 0 1-.34-5.58 2.5 2.5 0 0 1 1.32-4.24 2.5 2.5 0 0 1 1.98-3A2.5 2.5 0 0 1 9.5 2Z" />
255
+ <path d="M14.5 2A2.5 2.5 0 0 0 12 4.5v15a2.5 2.5 0 0 0 4.96.44 2.5 2.5 0 0 0 2.96-3.08 3 3 0 0 0 .34-5.58 2.5 2.5 0 0 0-1.32-4.24 2.5 2.5 0 0 0-1.98-3A2.5 2.5 0 0 0 14.5 2Z" />
256
+ </svg>
257
+ );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
 
259
+ export default AIInterview;
Frontend/tailwind.config.cjs CHANGED
@@ -5,20 +5,48 @@ module.exports = {
5
  ],
6
  theme: {
7
  extend: {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  animation: {
9
  spotlight: "spotlight 2s ease .75s 1 forwards",
 
 
10
  },
11
  keyframes: {
12
  spotlight: {
13
- "0%": {
14
- opacity: 0,
15
- transform: "translate(-72%, -62%) scale(0.5)",
16
- },
17
- "100%": {
18
- opacity: 1,
19
- transform: "translate(-50%,-40%) scale(1)",
20
- },
21
  },
 
 
 
 
22
  },
23
  },
24
  },
 
5
  ],
6
  theme: {
7
  extend: {
8
+ colors: {
9
+ maya: {
10
+ dark: "#696FC7", // Sidebar purple
11
+ light: "#A7AAE1", // Light purple buttons/accents
12
+ beige: "#F5D3C4", // Background accent
13
+ pink: "#F2AEBB", // Background accent
14
+ grid: "#cfd1e6", // Grid line color
15
+ }
16
+ },
17
+ backgroundImage: {
18
+ // Creates the "Graph Paper" sketchy look
19
+ 'sketchy-grid': `
20
+ linear-gradient(to right, #cfd1e6 1px, transparent 1px),
21
+ linear-gradient(to bottom, #cfd1e6 1px, transparent 1px)
22
+ `,
23
+ },
24
+ backgroundSize: {
25
+ 'sketchy': '24px 24px',
26
+ },
27
+ // Added Font Family for the Sketchy Look
28
+ fontFamily: {
29
+ handwriting: ['"Patrick Hand"', 'cursive'],
30
+ sans: ['"Open Sans"', 'sans-serif'],
31
+ },
32
  animation: {
33
  spotlight: "spotlight 2s ease .75s 1 forwards",
34
+ 'orb-float': "float 6s ease-in-out infinite",
35
+ 'orb-breathe': "breathe 4s ease-in-out infinite",
36
  },
37
  keyframes: {
38
  spotlight: {
39
+ "0%": { opacity: 0, transform: "translate(-72%, -62%) scale(0.5)" },
40
+ "100%": { opacity: 1, transform: "translate(-50%,-40%) scale(1)" },
41
+ },
42
+ float: {
43
+ "0%, 100%": { transform: "translateY(0px)" },
44
+ "50%": { transform: "translateY(-10px)" },
 
 
45
  },
46
+ breathe: {
47
+ "0%, 100%": { transform: "scale(1)" },
48
+ "50%": { transform: "scale(1.05)" },
49
+ }
50
  },
51
  },
52
  },