aki-008 commited on
Commit
b0803f3
·
1 Parent(s): 4460629

feat: vapi working

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, interview
3
 
4
  api_router = APIRouter()
5
 
 
23
  prefix="/notes",
24
  tags=["notes"]
25
  )
26
+
27
+ api_router.include_router(
28
+ interview.router,
29
+ prefix="/interview",
30
+ tags=["Interview"]
31
+ )
Backend/app/api/v1/endpoints/interview.py ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import uvicorn
3
+ from fastapi import APIRouter, HTTPException, Request
4
+ from fastapi.middleware.cors import CORSMiddleware
5
+ from pydantic import BaseModel
6
+ from app.config import settings
7
+ from vapi import Vapi
8
+ from dotenv import load_dotenv
9
+
10
+ # Load environment variables from .env file
11
+ load_dotenv()
12
+
13
+ router = APIRouter()
14
+
15
+
16
+ # --- CONFIGURATION ---
17
+ VAPI_PRIVATE_KEY = os.getenv("VAPI_PRIVATE_KEY")
18
+ VAPI_ASSISTANT_ID = os.getenv("VAPI_ASSISTANT_ID")
19
+ # The SERVER_URL MUST be set to your public ngrok HTTPS URL for external webhooks to work.
20
+ SERVER_URL = os.getenv("SERVER_URL", "http://localhost:8000")
21
+
22
+ # Initialize Vapi Server SDK
23
+ try:
24
+ vapi_server = Vapi(token=VAPI_PRIVATE_KEY)
25
+ 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
+
48
+ @router.post("/api/get-vapi-config")
49
+ async def get_vapi_config(data: ConfigRequest):
50
+ """
51
+ Endpoint called by the Frontend to get the dynamically generated Assistant configuration.
52
+ """
53
+ if VAPI_ASSISTANT_ID == "asst_11111111111111111111":
54
+ raise HTTPException(
55
+ status_code=503,
56
+ detail="VAPI_ASSISTANT_ID not configured in .env. Please set your ID."
57
+ )
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
77
+ assistant_overrides = {
78
+ "model": {
79
+ "provider": "openai",
80
+ "model": "gpt-4o-mini", # or "gpt-4", "gpt-3.5-turbo"
81
+ "messages": [
82
+ {"role": "system", "content": system_prompt}
83
+ ]
84
+ },
85
+ "server": {
86
+ "url": webhook_url
87
+ },
88
+ # ... keep metadata as is ...
89
+ "metadata": {
90
+ "user_name": data.name,
91
+ "job_role": data.job_role,
92
+ "environment": "standalone-test"
93
+ }
94
+ }
95
+
96
+ # 4. Return the necessary config to the frontend Web SDK
97
+ return {
98
+ "assistantId": VAPI_ASSISTANT_ID,
99
+ "overrides": assistant_overrides
100
+ }
101
+
102
+ except Exception as e:
103
+ print(f"❌ Vapi Configuration Error: {e}")
104
+ raise HTTPException(status_code=500, detail=f"Failed to configure agent: {str(e)}")
105
+
106
+
107
+ @router.post("/api/webhook")
108
+ async def vapi_webhook_receiver(request: Request):
109
+ """
110
+ Endpoint that receives asynchronous events from Vapi's servers.
111
+ """
112
+ payload = await request.json()
113
+ message = payload.get("message", {})
114
+
115
+ # Log incoming transcripts in real-time
116
+ if message.get("type") == "transcript" and message.get("transcriptType") == "final":
117
+ print(f"🗣️ [Transcript] {message.get('role').upper()}: {message.get('transcript')}")
118
+
119
+ # Log the final report (contains summary, full conversation, etc.)
120
+ elif message.get("type") == "end-of-call-report":
121
+ metadata = payload.get("assistant", {}).get("metadata", {})
122
+ print(f"\n--- 🏁 Call Ended Report ---")
123
+ print(f" User: {metadata.get('user_name')}, Role: {metadata.get('job_role')}")
124
+ print(f" Summary: {message.get('summary', 'N/A')}")
125
+ print(f"---------------------------\n")
126
+
127
+ # Vapi expects a 200 OK response
128
+ return {"status": "ok"}
129
+
130
+ @router.get("/")
131
+ async def root():
132
+ return {"message": "Vapi Standalone Backend is running on port 8000."}
133
+
134
+ if __name__ == "__main__":
135
+ uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
Backend/app/config.py CHANGED
@@ -18,6 +18,10 @@ 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 = "your-vapi-assistant-id"
22
+ VAPI_PRIVATE_KEY: str
23
+ VAPI_PUBLIC_KEY: str
24
+
25
  class Config:
26
  env_file = ".env"
27
  extra = "ignore" # quiz
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/package-lock.json CHANGED
@@ -12,6 +12,7 @@
12
  "@splinetool/react-spline": "^4.1.0",
13
  "@splinetool/runtime": "^1.11.2",
14
  "@tailwindcss/vite": "^4.1.17",
 
15
  "axios": "^1.13.2",
16
  "clsx": "^2.1.1",
17
  "dayjs": "^1.11.19",
@@ -73,7 +74,6 @@
73
  "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
74
  "dev": true,
75
  "license": "MIT",
76
- "peer": true,
77
  "dependencies": {
78
  "@babel/code-frame": "^7.27.1",
79
  "@babel/generator": "^7.28.5",
@@ -277,6 +277,15 @@
277
  "@babel/core": "^7.0.0-0"
278
  }
279
  },
 
 
 
 
 
 
 
 
 
280
  "node_modules/@babel/template": {
281
  "version": "7.27.2",
282
  "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
@@ -325,6 +334,22 @@
325
  "node": ">=6.9.0"
326
  }
327
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
328
  "node_modules/@esbuild/aix-ppc64": {
329
  "version": "0.25.12",
330
  "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
@@ -1537,6 +1562,81 @@
1537
  "win32"
1538
  ]
1539
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1540
  "node_modules/@splinetool/react-spline": {
1541
  "version": "4.1.0",
1542
  "resolved": "https://registry.npmjs.org/@splinetool/react-spline/-/react-spline-4.1.0.tgz",
@@ -1563,7 +1663,6 @@
1563
  "version": "1.11.2",
1564
  "resolved": "https://registry.npmjs.org/@splinetool/runtime/-/runtime-1.11.2.tgz",
1565
  "integrity": "sha512-rFz3KOQQRHQGzWBvPKRZcI7fZe5qxNYX1FmmCqzsbJkAU/hJdifaxpyN4xESpbkdta6s7riSmoz5lmPGIpZRRQ==",
1566
- "peer": true,
1567
  "dependencies": {
1568
  "on-change": "^4.0.0",
1569
  "semver-compare": "^1.0.0"
@@ -2007,7 +2106,6 @@
2007
  "integrity": "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==",
2008
  "devOptional": true,
2009
  "license": "MIT",
2010
- "peer": true,
2011
  "dependencies": {
2012
  "undici-types": "~7.16.0"
2013
  }
@@ -2017,7 +2115,6 @@
2017
  "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
2018
  "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
2019
  "license": "MIT",
2020
- "peer": true,
2021
  "dependencies": {
2022
  "csstype": "^3.0.2"
2023
  }
@@ -2090,7 +2187,6 @@
2090
  "integrity": "sha512-6m1I5RmHBGTnUGS113G04DMu3CpSdxCAU/UvtjNWL4Nuf3MW9tQhiJqRlHzChIkhy6kZSAQmc+I1bcGjE3yNKg==",
2091
  "dev": true,
2092
  "license": "MIT",
2093
- "peer": true,
2094
  "dependencies": {
2095
  "@typescript-eslint/scope-manager": "8.46.3",
2096
  "@typescript-eslint/types": "8.46.3",
@@ -2322,6 +2418,19 @@
2322
  "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
2323
  "license": "ISC"
2324
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
2325
  "node_modules/@vitejs/plugin-react": {
2326
  "version": "5.1.0",
2327
  "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.0.tgz",
@@ -2349,7 +2458,6 @@
2349
  "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
2350
  "dev": true,
2351
  "license": "MIT",
2352
- "peer": true,
2353
  "bin": {
2354
  "acorn": "bin/acorn"
2355
  },
@@ -2457,6 +2565,12 @@
2457
  "integrity": "sha512-cRygWd7kGBQO3VEhPiTgq4Wc43ctsM+o46urrmPOiuAe+07fzlSB9OJVdpgDL0jPqXUVQ9ht7aq7kxOeJHRK+w==",
2458
  "license": "MIT"
2459
  },
 
 
 
 
 
 
2460
  "node_modules/brace-expansion": {
2461
  "version": "1.1.12",
2462
  "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
@@ -2501,7 +2615,6 @@
2501
  }
2502
  ],
2503
  "license": "MIT",
2504
- "peer": true,
2505
  "dependencies": {
2506
  "baseline-browser-mapping": "^2.8.19",
2507
  "caniuse-lite": "^1.0.30001751",
@@ -3091,7 +3204,6 @@
3091
  "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
3092
  "dev": true,
3093
  "license": "MIT",
3094
- "peer": true,
3095
  "dependencies": {
3096
  "@eslint-community/eslint-utils": "^4.8.0",
3097
  "@eslint-community/regexpp": "^4.12.1",
@@ -3279,6 +3391,15 @@
3279
  "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
3280
  "license": "MIT"
3281
  },
 
 
 
 
 
 
 
 
 
3282
  "node_modules/extend": {
3283
  "version": "3.0.2",
3284
  "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
@@ -5483,7 +5604,6 @@
5483
  "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
5484
  "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
5485
  "license": "MIT",
5486
- "peer": true,
5487
  "engines": {
5488
  "node": ">=0.10.0"
5489
  }
@@ -5493,7 +5613,6 @@
5493
  "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
5494
  "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
5495
  "license": "MIT",
5496
- "peer": true,
5497
  "dependencies": {
5498
  "scheduler": "^0.27.0"
5499
  },
@@ -5550,7 +5669,6 @@
5550
  "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
5551
  "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
5552
  "license": "MIT",
5553
- "peer": true,
5554
  "dependencies": {
5555
  "@types/use-sync-external-store": "^0.0.6",
5556
  "use-sync-external-store": "^1.4.0"
@@ -5651,8 +5769,7 @@
5651
  "version": "5.0.1",
5652
  "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
5653
  "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
5654
- "license": "MIT",
5655
- "peer": true
5656
  },
5657
  "node_modules/redux-thunk": {
5658
  "version": "3.1.0",
@@ -6028,7 +6145,6 @@
6028
  "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
6029
  "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
6030
  "license": "MIT",
6031
- "peer": true,
6032
  "engines": {
6033
  "node": ">=12"
6034
  },
@@ -6107,7 +6223,6 @@
6107
  "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
6108
  "dev": true,
6109
  "license": "Apache-2.0",
6110
- "peer": true,
6111
  "bin": {
6112
  "tsc": "bin/tsc",
6113
  "tsserver": "bin/tsserver"
@@ -6339,7 +6454,6 @@
6339
  "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz",
6340
  "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
6341
  "license": "MIT",
6342
- "peer": true,
6343
  "dependencies": {
6344
  "esbuild": "^0.25.0",
6345
  "fdir": "^6.5.0",
@@ -6431,7 +6545,6 @@
6431
  "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
6432
  "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
6433
  "license": "MIT",
6434
- "peer": true,
6435
  "engines": {
6436
  "node": ">=12"
6437
  },
 
12
  "@splinetool/react-spline": "^4.1.0",
13
  "@splinetool/runtime": "^1.11.2",
14
  "@tailwindcss/vite": "^4.1.17",
15
+ "@vapi-ai/web": "^2.5.2",
16
  "axios": "^1.13.2",
17
  "clsx": "^2.1.1",
18
  "dayjs": "^1.11.19",
 
74
  "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
75
  "dev": true,
76
  "license": "MIT",
 
77
  "dependencies": {
78
  "@babel/code-frame": "^7.27.1",
79
  "@babel/generator": "^7.28.5",
 
277
  "@babel/core": "^7.0.0-0"
278
  }
279
  },
280
+ "node_modules/@babel/runtime": {
281
+ "version": "7.28.4",
282
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
283
+ "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
284
+ "license": "MIT",
285
+ "engines": {
286
+ "node": ">=6.9.0"
287
+ }
288
+ },
289
  "node_modules/@babel/template": {
290
  "version": "7.27.2",
291
  "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
 
334
  "node": ">=6.9.0"
335
  }
336
  },
337
+ "node_modules/@daily-co/daily-js": {
338
+ "version": "0.85.0",
339
+ "resolved": "https://registry.npmjs.org/@daily-co/daily-js/-/daily-js-0.85.0.tgz",
340
+ "integrity": "sha512-lpl111ZWNTUWDnwYcPuNi9PGJPbLCeCw6LzmEY40nG0hv1jg5JLVW8Rq3Cj/+lOCP6W6h4PXm211ss0FFnxITQ==",
341
+ "license": "BSD-2-Clause",
342
+ "dependencies": {
343
+ "@babel/runtime": "^7.12.5",
344
+ "@sentry/browser": "^8.33.1",
345
+ "bowser": "^2.8.1",
346
+ "dequal": "^2.0.3",
347
+ "events": "^3.1.0"
348
+ },
349
+ "engines": {
350
+ "node": ">=10.0.0"
351
+ }
352
+ },
353
  "node_modules/@esbuild/aix-ppc64": {
354
  "version": "0.25.12",
355
  "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
 
1562
  "win32"
1563
  ]
1564
  },
1565
+ "node_modules/@sentry-internal/browser-utils": {
1566
+ "version": "8.55.0",
1567
+ "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-8.55.0.tgz",
1568
+ "integrity": "sha512-ROgqtQfpH/82AQIpESPqPQe0UyWywKJsmVIqi3c5Fh+zkds5LUxnssTj3yNd1x+kxaPDVB023jAP+3ibNgeNDw==",
1569
+ "license": "MIT",
1570
+ "dependencies": {
1571
+ "@sentry/core": "8.55.0"
1572
+ },
1573
+ "engines": {
1574
+ "node": ">=14.18"
1575
+ }
1576
+ },
1577
+ "node_modules/@sentry-internal/feedback": {
1578
+ "version": "8.55.0",
1579
+ "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-8.55.0.tgz",
1580
+ "integrity": "sha512-cP3BD/Q6pquVQ+YL+rwCnorKuTXiS9KXW8HNKu4nmmBAyf7urjs+F6Hr1k9MXP5yQ8W3yK7jRWd09Yu6DHWOiw==",
1581
+ "license": "MIT",
1582
+ "dependencies": {
1583
+ "@sentry/core": "8.55.0"
1584
+ },
1585
+ "engines": {
1586
+ "node": ">=14.18"
1587
+ }
1588
+ },
1589
+ "node_modules/@sentry-internal/replay": {
1590
+ "version": "8.55.0",
1591
+ "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-8.55.0.tgz",
1592
+ "integrity": "sha512-roCDEGkORwolxBn8xAKedybY+Jlefq3xYmgN2fr3BTnsXjSYOPC7D1/mYqINBat99nDtvgFvNfRcZPiwwZ1hSw==",
1593
+ "license": "MIT",
1594
+ "dependencies": {
1595
+ "@sentry-internal/browser-utils": "8.55.0",
1596
+ "@sentry/core": "8.55.0"
1597
+ },
1598
+ "engines": {
1599
+ "node": ">=14.18"
1600
+ }
1601
+ },
1602
+ "node_modules/@sentry-internal/replay-canvas": {
1603
+ "version": "8.55.0",
1604
+ "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-8.55.0.tgz",
1605
+ "integrity": "sha512-nIkfgRWk1091zHdu4NbocQsxZF1rv1f7bbp3tTIlZYbrH62XVZosx5iHAuZG0Zc48AETLE7K4AX9VGjvQj8i9w==",
1606
+ "license": "MIT",
1607
+ "dependencies": {
1608
+ "@sentry-internal/replay": "8.55.0",
1609
+ "@sentry/core": "8.55.0"
1610
+ },
1611
+ "engines": {
1612
+ "node": ">=14.18"
1613
+ }
1614
+ },
1615
+ "node_modules/@sentry/browser": {
1616
+ "version": "8.55.0",
1617
+ "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-8.55.0.tgz",
1618
+ "integrity": "sha512-1A31mCEWCjaMxJt6qGUK+aDnLDcK6AwLAZnqpSchNysGni1pSn1RWSmk9TBF8qyTds5FH8B31H480uxMPUJ7Cw==",
1619
+ "license": "MIT",
1620
+ "dependencies": {
1621
+ "@sentry-internal/browser-utils": "8.55.0",
1622
+ "@sentry-internal/feedback": "8.55.0",
1623
+ "@sentry-internal/replay": "8.55.0",
1624
+ "@sentry-internal/replay-canvas": "8.55.0",
1625
+ "@sentry/core": "8.55.0"
1626
+ },
1627
+ "engines": {
1628
+ "node": ">=14.18"
1629
+ }
1630
+ },
1631
+ "node_modules/@sentry/core": {
1632
+ "version": "8.55.0",
1633
+ "resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.55.0.tgz",
1634
+ "integrity": "sha512-6g7jpbefjHYs821Z+EBJ8r4Z7LT5h80YSWRJaylGS4nW5W5Z2KXzpdnyFarv37O7QjauzVC2E+PABmpkw5/JGA==",
1635
+ "license": "MIT",
1636
+ "engines": {
1637
+ "node": ">=14.18"
1638
+ }
1639
+ },
1640
  "node_modules/@splinetool/react-spline": {
1641
  "version": "4.1.0",
1642
  "resolved": "https://registry.npmjs.org/@splinetool/react-spline/-/react-spline-4.1.0.tgz",
 
1663
  "version": "1.11.2",
1664
  "resolved": "https://registry.npmjs.org/@splinetool/runtime/-/runtime-1.11.2.tgz",
1665
  "integrity": "sha512-rFz3KOQQRHQGzWBvPKRZcI7fZe5qxNYX1FmmCqzsbJkAU/hJdifaxpyN4xESpbkdta6s7riSmoz5lmPGIpZRRQ==",
 
1666
  "dependencies": {
1667
  "on-change": "^4.0.0",
1668
  "semver-compare": "^1.0.0"
 
2106
  "integrity": "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==",
2107
  "devOptional": true,
2108
  "license": "MIT",
 
2109
  "dependencies": {
2110
  "undici-types": "~7.16.0"
2111
  }
 
2115
  "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
2116
  "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
2117
  "license": "MIT",
 
2118
  "dependencies": {
2119
  "csstype": "^3.0.2"
2120
  }
 
2187
  "integrity": "sha512-6m1I5RmHBGTnUGS113G04DMu3CpSdxCAU/UvtjNWL4Nuf3MW9tQhiJqRlHzChIkhy6kZSAQmc+I1bcGjE3yNKg==",
2188
  "dev": true,
2189
  "license": "MIT",
 
2190
  "dependencies": {
2191
  "@typescript-eslint/scope-manager": "8.46.3",
2192
  "@typescript-eslint/types": "8.46.3",
 
2418
  "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
2419
  "license": "ISC"
2420
  },
2421
+ "node_modules/@vapi-ai/web": {
2422
+ "version": "2.5.2",
2423
+ "resolved": "https://registry.npmjs.org/@vapi-ai/web/-/web-2.5.2.tgz",
2424
+ "integrity": "sha512-mT4DjApi0/0+EK77h2xOLq3qVBa7rv3JNQ+gFWuhFE4YdGYxI51+fn8bQI9N535+zU/Z4jFKXotdBHQJ3filHA==",
2425
+ "license": "MIT",
2426
+ "dependencies": {
2427
+ "@daily-co/daily-js": "^0.85.0",
2428
+ "events": "^3.3.0"
2429
+ },
2430
+ "engines": {
2431
+ "node": ">=18.0.0"
2432
+ }
2433
+ },
2434
  "node_modules/@vitejs/plugin-react": {
2435
  "version": "5.1.0",
2436
  "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.0.tgz",
 
2458
  "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
2459
  "dev": true,
2460
  "license": "MIT",
 
2461
  "bin": {
2462
  "acorn": "bin/acorn"
2463
  },
 
2565
  "integrity": "sha512-cRygWd7kGBQO3VEhPiTgq4Wc43ctsM+o46urrmPOiuAe+07fzlSB9OJVdpgDL0jPqXUVQ9ht7aq7kxOeJHRK+w==",
2566
  "license": "MIT"
2567
  },
2568
+ "node_modules/bowser": {
2569
+ "version": "2.13.1",
2570
+ "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.13.1.tgz",
2571
+ "integrity": "sha512-OHawaAbjwx6rqICCKgSG0SAnT05bzd7ppyKLVUITZpANBaaMFBAsaNkto3LoQ31tyFP5kNujE8Cdx85G9VzOkw==",
2572
+ "license": "MIT"
2573
+ },
2574
  "node_modules/brace-expansion": {
2575
  "version": "1.1.12",
2576
  "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
 
2615
  }
2616
  ],
2617
  "license": "MIT",
 
2618
  "dependencies": {
2619
  "baseline-browser-mapping": "^2.8.19",
2620
  "caniuse-lite": "^1.0.30001751",
 
3204
  "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
3205
  "dev": true,
3206
  "license": "MIT",
 
3207
  "dependencies": {
3208
  "@eslint-community/eslint-utils": "^4.8.0",
3209
  "@eslint-community/regexpp": "^4.12.1",
 
3391
  "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
3392
  "license": "MIT"
3393
  },
3394
+ "node_modules/events": {
3395
+ "version": "3.3.0",
3396
+ "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
3397
+ "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
3398
+ "license": "MIT",
3399
+ "engines": {
3400
+ "node": ">=0.8.x"
3401
+ }
3402
+ },
3403
  "node_modules/extend": {
3404
  "version": "3.0.2",
3405
  "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
 
5604
  "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
5605
  "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
5606
  "license": "MIT",
 
5607
  "engines": {
5608
  "node": ">=0.10.0"
5609
  }
 
5613
  "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
5614
  "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
5615
  "license": "MIT",
 
5616
  "dependencies": {
5617
  "scheduler": "^0.27.0"
5618
  },
 
5669
  "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
5670
  "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
5671
  "license": "MIT",
 
5672
  "dependencies": {
5673
  "@types/use-sync-external-store": "^0.0.6",
5674
  "use-sync-external-store": "^1.4.0"
 
5769
  "version": "5.0.1",
5770
  "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
5771
  "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
5772
+ "license": "MIT"
 
5773
  },
5774
  "node_modules/redux-thunk": {
5775
  "version": "3.1.0",
 
6145
  "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
6146
  "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
6147
  "license": "MIT",
 
6148
  "engines": {
6149
  "node": ">=12"
6150
  },
 
6223
  "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
6224
  "dev": true,
6225
  "license": "Apache-2.0",
 
6226
  "bin": {
6227
  "tsc": "bin/tsc",
6228
  "tsserver": "bin/tsserver"
 
6454
  "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz",
6455
  "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
6456
  "license": "MIT",
 
6457
  "dependencies": {
6458
  "esbuild": "^0.25.0",
6459
  "fdir": "^6.5.0",
 
6545
  "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
6546
  "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
6547
  "license": "MIT",
 
6548
  "engines": {
6549
  "node": ">=12"
6550
  },
Frontend/package.json CHANGED
@@ -13,6 +13,7 @@
13
  "@splinetool/react-spline": "^4.1.0",
14
  "@splinetool/runtime": "^1.11.2",
15
  "@tailwindcss/vite": "^4.1.17",
 
16
  "axios": "^1.13.2",
17
  "clsx": "^2.1.1",
18
  "dayjs": "^1.11.19",
 
13
  "@splinetool/react-spline": "^4.1.0",
14
  "@splinetool/runtime": "^1.11.2",
15
  "@tailwindcss/vite": "^4.1.17",
16
+ "@vapi-ai/web": "^2.5.2",
17
  "axios": "^1.13.2",
18
  "clsx": "^2.1.1",
19
  "dayjs": "^1.11.19",
Frontend/src/api/interviewService.ts ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import API from "./api";
2
+
3
+ export const getVapiConfig = async (
4
+ name: string,
5
+ jobRole: string,
6
+ experience: number,
7
+ level: string
8
+ ) => {
9
+ const response = await API.post("/interview/config", {
10
+ name,
11
+ job_role: jobRole,
12
+ experience,
13
+ level,
14
+ });
15
+ return response.data;
16
+ };
Frontend/src/pages/AiInterview.tsx CHANGED
@@ -1,155 +1,275 @@
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"
51
- 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"
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
59
- value={level}
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
137
- </button>
 
 
 
 
 
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, { 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
+
98
+ const stopInterview = () => {
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;