Skip to content

Commit 39ec3ce

Browse files
committed
feat: implement AI interview session component with real-time speech and timer features
1 parent cea3722 commit 39ec3ce

1 file changed

Lines changed: 77 additions & 16 deletions

File tree

client/src/pages/AIInterview.jsx

Lines changed: 77 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,37 @@ const AIInterview = () => {
3030
const [timerActive, setTimerActive] = useState(false);
3131
const [continuousMode, setContinuousMode] = useState(true);
3232
const [isAiSpeaking, setIsAiSpeaking] = useState(false);
33+
const [voices, setVoices] = useState([]);
34+
const [selectedVoiceName, setSelectedVoiceName] = useState('');
35+
36+
useEffect(() => {
37+
const updateVoices = () => {
38+
const availableVoices = window.speechSynthesis.getVoices().filter(v => v.lang.startsWith('en'));
39+
setVoices(availableVoices);
40+
41+
// Try to auto-select a premium natural/online voice if none selected yet
42+
if (availableVoices.length > 0) {
43+
setSelectedVoiceName(prev => {
44+
if (prev) return prev;
45+
const premium = availableVoices.find(v =>
46+
v.name.includes('Natural') ||
47+
v.name.includes('Online') ||
48+
v.name.includes('Google') ||
49+
v.name.includes('Aria') ||
50+
v.name.includes('Guy') ||
51+
v.name.includes('Samantha') ||
52+
v.name.includes('Daniel')
53+
) || availableVoices[0];
54+
return premium.name;
55+
});
56+
}
57+
};
58+
59+
updateVoices();
60+
if (window.speechSynthesis.onvoiceschanged !== undefined) {
61+
window.speechSynthesis.onvoiceschanged = updateVoices;
62+
}
63+
}, []);
3364

3465
const recognitionRef = useRef(null);
3566
const idleTimerRef = useRef(null);
@@ -253,14 +284,9 @@ const AIInterview = () => {
253284
const utter = new SpeechSynthesisUtterance(text);
254285
utter.lang = 'en-US';
255286

256-
const voices = window.speechSynthesis.getVoices();
257-
const preferredVoice = voices.find(v =>
258-
v.lang.startsWith('en-') &&
259-
(v.name.includes('Google') || v.name.includes('Natural') || v.name.includes('Microsoft') || v.name.includes('Samantha') || v.name.includes('Daniel'))
260-
) || voices.find(v => v.lang.startsWith('en-'));
261-
262-
if (preferredVoice) {
263-
utter.voice = preferredVoice;
287+
const activeVoice = voices.find(v => v.name === selectedVoiceName) || voices.find(v => v.lang.startsWith('en-'));
288+
if (activeVoice) {
289+
utter.voice = activeVoice;
264290
}
265291
utter.pitch = 1.05;
266292
utter.rate = 0.95; // Slightly faster to sound more natural
@@ -416,7 +442,13 @@ Generate ONE highly relevant, specific, and challenging interview question stric
416442
Use the candidate's resume context below ONLY to calibrate the experience level (Junior, Mid, Senior) and the context of the question.
417443
If the topic is "Data Structures & Algorithms", ask a specific conceptual or coding/problem-solving question (e.g., about arrays, trees, dynamic programming, complexity, etc.) rather than asking about their previous projects.
418444
${exclusionsPrompt}
419-
Respond with ONLY the question text itself. No introductory remarks, no meta-commentary, no markdown formatting.
445+
446+
Tone Instructions:
447+
- You must adopt a tone that is extremely polite, humble, encouraging, and friendly.
448+
- Start the response with a very brief, warm, conversational greeting or transition (e.g., "Hi! Let's take a look at a question about...", "Awesome, I'd love to discuss...", "I hope you are ready for a fun question regarding...") to make the candidate feel comfortable.
449+
- Avoid sounding strict, robotic, or overly formal. Keep it supportive like a mentor.
450+
451+
Respond with ONLY the conversational spoken text itself. No meta-commentary, no markdown formatting.
420452
421453
Candidate Resume Context:
422454
${resumeText}
@@ -450,21 +482,20 @@ Question:`;
450482
const evaluateAnswer = async () => {
451483
setLoading(true);
452484
setError('');
453-
const prompt = `You are a professional, supportive, and constructive technical interviewer.
485+
const prompt = `You are an extremely polite, humble, warm, and friendly technical interviewer.
454486
Please evaluate the candidate's response to your question.
455487
456488
Question: "${question}"
457489
Candidate Answer: "${answer}"
458490
459491
Instructions:
460-
1. Act like a professional interviewer: be polite, encouraging, and supportive. Do not use scary or overly critical language. Make the candidate feel motivated.
461-
2. If the candidate missed key parts, made mistake(s), or said they don't know: politely correct them and explicitly provide the correct answer, recommended coding logic, or best-practice solution in your feedback.
462-
3. Keep the feedback structured, clear, and educational.
463-
4. Return ONLY a JSON object of shape:
492+
1. Act like a supportive peer or a humble mentor. Speak in an encouraging, warm, and highly friendly tone.
493+
2. If the candidate made mistakes, missed details, or skipped: gently and politely guide them. Never sound harsh, critical, or condescending. Keep them fully motivated!
494+
3. Return ONLY a JSON object of shape:
464495
{
465496
"correct": boolean,
466-
"feedback": "constructive feedback text containing corrections and correct answers/solutions",
467-
"voiceResponse": "A very short, natural, conversational spoken feedback (1-2 sentences max) to say to the candidate. E.g., 'That is a correct answer! You explained the concept well.' or 'Good effort, but not quite. We actually use a hash map here to achieve constant time complexity. Let's move forward.'"
497+
"feedback": "A polite, constructive, and educational text feedback.",
498+
"voiceResponse": "A very short, warm, and conversational feedback (1-2 sentences max) to say out loud. It MUST be humble and friendly (e.g., 'That's a really great start! I love how you thought about that.', 'Wow, spot on! You explained that beautifully. Let's try the next one.', 'No worries at all, this can be tricky! Actually, we can look at it as...')."
468499
}
469500
Do not include any markdown backticks, introductory text, or styling in your raw output.
470501
@@ -1076,6 +1107,36 @@ JSON Evaluation:`;
10761107
</div>
10771108
</div>
10781109

1110+
{/* Voice Style Selector */}
1111+
{voices.length > 0 && (
1112+
<div className="space-y-1">
1113+
<label className="label font-bold flex items-center justify-between text-xs text-dark-500">
1114+
<span>Interviewer Voice (Human-like Edge/Chrome/Safari voices)</span>
1115+
<button
1116+
type="button"
1117+
onClick={() => speak("Hello! I am your friendly AI technical interviewer. How does my voice sound to you?")}
1118+
className="text-[10px] text-emerald-500 hover:text-emerald-600 font-extrabold flex items-center gap-1 cursor-pointer bg-transparent border-none p-0"
1119+
>
1120+
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1121+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2.5" d="M15.536 8.464a5 5 0 010 7.072m2.828-9.9a9 9 0 010 12.728M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L5.586 15z" />
1122+
</svg>
1123+
Listen / Test Voice
1124+
</button>
1125+
</label>
1126+
<select
1127+
className="input text-xs"
1128+
value={selectedVoiceName}
1129+
onChange={(e) => setSelectedVoiceName(e.target.value)}
1130+
>
1131+
{voices.map((v, idx) => (
1132+
<option key={idx} value={v.name}>
1133+
{v.name} ({v.lang}) {v.name.includes('Natural') || v.name.includes('Online') ? '✨ Premium Natural' : ''}
1134+
</option>
1135+
))}
1136+
</select>
1137+
</div>
1138+
)}
1139+
10791140
{/* Hands-Free Interactive Settings */}
10801141
<div className="p-4 rounded-xl bg-primary-500/5 border border-primary-500/10 flex items-center justify-between gap-4">
10811142
<div className="space-y-0.5">

0 commit comments

Comments
 (0)