import React, { useState, useRef, useEffect } from 'react';
import { Play, Square, Video, ShieldCheck, Activity, Award, ChevronRight, RefreshCw, Volume2, User } from 'lucide-react';
import { initializeApp } from 'firebase/app';
import { getAuth, signInAnonymously } from 'firebase/auth';
import { getFirestore, collection, addDoc, doc, setDoc } from 'firebase/firestore';
// --- API Configuration ---
const apiKey = ""; // Runtime provides the key
const App = () => {
const [status, setStatus] = useState('idle'); // idle, speaking, recording, analyzing, results
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
const [evaluation, setEvaluation] = useState(null);
const [isRecording, setIsRecording] = useState(false);
const [videoBlobURL, setVideoBlobURL] = useState(null);
const [error, setError] = useState(null);
const mediaRecorderRef = useRef(null);
const videoChunksRef = useRef([]);
const liveVideoRef = useRef(null);
const streamRef = useRef(null);
const questions = [
{
id: 1,
text: "Please introduce yourself and tell us why you want to be an English teacher in Japan.",
focus: "Fluency, Intonation & Overall Impression"
},
{
id: 2,
text: "Read the following sentence with emotion: 'The sophisticated synchronization of educational technology and pedagogical expertise creates a superior learning environment.'",
focus: "Complex Pronunciation, Clarity & Expressiveness"
},
{
id: 3,
text: "How would you handle a student who is very shy and doesn't want to speak in class? Show empathy in your response.",
focus: "Communication Style, Empathy & Non-verbal Cues"
}
];
// --- Utilities ---
// pcmToWav is not needed for video analysis
const playTTS = async (text) => {
setStatus('speaking');
try {
const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-tts:generateContent?key=${apiKey}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contents: [{ parts: [{ text: `Say professionally: ${text}` }] }],
generationConfig: {
responseModalities: ["AUDIO"],
speechConfig: { voiceConfig: { prebuiltVoiceConfig: { voiceName: "Kore" } } }
}
})
});
const result = await response.json();
const audioData = result.candidates[0].content.parts[0].inlineData.data;
const binaryString = atob(audioData);
const arrayBuffer = new ArrayBuffer(binaryString.length);
const uint8Array = new Uint8Array(arrayBuffer);
for (let i = 0; i < binaryString.length; i++) {
uint8Array[i] = binaryString.charCodeAt(i);
}
const blob = new Blob([uint8Array], { type: 'audio/wav' });
const url = URL.createObjectURL(blob);
const audio = new Audio(url);
audio.onended = () => setStatus('idle');
audio.play();
} catch (err) {
console.error(err);
setStatus('idle');
setError("TTS playback failed.");
}
};
const startRecording = async () => {
try {
// Request both audio and video
const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: { width: 640, height: 480, facingMode: "user" } });
streamRef.current = stream;
// Show live video preview
if (liveVideoRef.current) {
liveVideoRef.current.srcObject = stream;
}
mediaRecorderRef.current = new MediaRecorder(stream, { mimeType: 'video/webm' });
videoChunksRef.current = [];
mediaRecorderRef.current.ondataavailable = (event) => {
if (event.data.size > 0) {
videoChunksRef.current.push(event.data);
}
};
mediaRecorderRef.current.onstop = async () => {
// Stop all tracks to turn off camera/mic
streamRef.current.getTracks().forEach(track => track.stop());
if (liveVideoRef.current) {
liveVideoRef.current.srcObject = null;
}
const videoBlob = new Blob(videoChunksRef.current, { type: 'video/webm' });
setVideoBlobURL(URL.createObjectURL(videoBlob));
const reader = new FileReader();
reader.readAsDataURL(videoBlob);
reader.onloadend = () => {
const base64Video = reader.result.split(',')[1];
evaluatePerformance(base64Video);
};
};
mediaRecorderRef.current.start();
setIsRecording(true);
setStatus('recording');
setError(null);
} catch (err) {
console.error(err);
setError("Camera/Microphone access denied. Please check permissions.");
}
};
const stopRecording = () => {
if (mediaRecorderRef.current && mediaRecorderRef.current.state !== 'inactive') {
mediaRecorderRef.current.stop();
setIsRecording(false);
setStatus('analyzing');
}
};
const evaluatePerformance = async (base64Video) => {
const prompt = `
As a professional linguistic expert and ALT recruiter, evaluate this applicant's video response for the following question: "${questions[currentQuestionIndex].text}".
Analyze BOTH the audio (pronunciation, fluency) AND the visual elements (facial expressions, eye contact, confidence, professional demeanor).
Provide a structured JSON response with:
- overall_score (0-100)
- nativeness_score (0-100: audio focus)
- clarity_score (0-100: audio focus)
- fluency_score (0-100: audio focus)
- expression_score (0-100: visual focus - confidence, engagement, appropriate facial cues)
- detailed_feedback (Japanese, professional tone, covering both speech and non-verbal aspects)
- transcription (The text you heard)
- areas_for_improvement (Array of specific points, mixed audio/visual)
`;
try {
const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent?key=${apiKey}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contents: [{
parts: [
{ text: prompt },
{ inlineData: { mimeType: "video/webm", data: base64Video } }
]
}],
generationConfig: { responseMimeType: "application/json" }
})
});
const data = await response.json();
if (!data.candidates || !data.candidates[0].content.parts[0].text) {
throw new Error("No analysis result generated.");
}
const evalData = JSON.parse(data.candidates[0].content.parts[0].text);
setEvaluation(evalData);
setStatus('results');
} catch (err) {
console.error(err);
setError("Analysis failed. The video might be too long or unclear. Please try again.");
setStatus('idle');
setVideoBlobURL(null);
}
};
const nextQuestion = () => {
if (currentQuestionIndex < questions.length - 1) {
setCurrentQuestionIndex(prev => prev + 1);
setEvaluation(null);
setVideoBlobURL(null);
setStatus('idle');
} else {
setStatus('finished');
}
};
// Cleanup stream on unmount
useEffect(() => {
return () => {
if (streamRef.current) {
streamRef.current.getTracks().forEach(track => track.stop());
}
};
}, []);
return (
{/* Header */}
SmartRecruit AI Video Interviewer
Candidate: #ALT-PRO-0293
{/* Main Content */}
{/* Left Sidebar - Progress & Notes */}
Interview Progress
{questions.map((q, idx) => (
{idx + 1}
Section {idx + 1}
{q.focus}
))}
Evaluation Criteria
"Assess not just WHAT they say, but HOW they say it. Look for confidence, natural engagement, and professional demeanor in their video response."
{/* Right Content - Interview Interaction Area */}
{status === 'finished' ? (
Interview Completed
Video responses and AI evaluations have been finalized for review.
) : (
<>
{/* Question Section */}
Question {currentQuestionIndex + 1} of {questions.length}
{questions[currentQuestionIndex].text}
{/* Video/Action Section */}
{status === 'recording' ? (
// Recording State: Live Preview
) : status === 'analyzing' ? (
// Analyzing State
AI Analyzing Video...
Evaluating speech clarity, fluency, and non-verbal communication cues.
) : status === 'results' && evaluation ? (
// Results State: Recorded Video & Scores
{videoBlobURL && }
Nativeness
{evaluation.nativeness_score}
Clarity
{evaluation.clarity_score}
Fluency
{evaluation.fluency_score}
{/* New Metric */}
Expression
{evaluation.expression_score}
AI Feedback (Audio & Visual)
{evaluation.detailed_feedback}
{evaluation.areas_for_improvement.map((item, i) => (
{item}
))}
) : (
// Idle State: Record Button
)}
{status === 'recording' && (
)}
>
)}
{error && (
)}
);
};
export default App;