'use client'; import { useState, useEffect, useRef, useCallback } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { useRouter } from 'next/navigation'; import { useInterview } from '@/context/InterviewContext'; import LoadingScreen from '@/components/LoadingScreen'; import ScoreRing from '@/components/ScoreRing'; import { Question, ChatMessage, Evaluation } from '@/types/interview'; import { Send, Mic, MicOff, Clock, ChevronRight, CheckCircle2, AlertCircle, Lightbulb, XCircle, SkipForward, Trophy, MessageSquare, Volume2, } from 'lucide-react'; export default function SessionPage() { const router = useRouter(); const { session, setQuestions, addMessage, submitAnswer, addEvaluation, nextQuestion, setStatus, completeSession, } = useInterview(); const [input, setInput] = useState(''); const [isLoading, setIsLoading] = useState(true); const [isEvaluating, setIsEvaluating] = useState(false); const [isGeneratingSummary, setIsGeneratingSummary] = useState(false); const [showEvaluation, setShowEvaluation] = useState(null); const [elapsedTime, setElapsedTime] = useState(0); const [isListening, setIsListening] = useState(false); const [error, setError] = useState(null); const messagesEndRef = useRef(null); const inputRef = useRef(null); // eslint-disable-next-line @typescript-eslint/no-explicit-any const recognitionRef = useRef(null); // Redirect if no session useEffect(() => { if (!session) { router.push('/interview'); } }, [session, router]); // Timer useEffect(() => { if (session?.status === 'in-progress') { const interval = setInterval(() => { const elapsed = Math.floor((Date.now() - session.startedAt) / 1000); setElapsedTime(elapsed); // Auto-end if time limit is set and exceeded if (session.config.timeLimit > 0) { const limitSeconds = session.config.timeLimit * 60; if (elapsed >= limitSeconds) { clearInterval(interval); handleEndInterview(); } } }, 1000); return () => clearInterval(interval); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [session?.status, session?.startedAt]); // Auto scroll to bottom useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [session?.messages]); // Generate questions on mount useEffect(() => { if (session && session.questions.length === 0 && isLoading) { generateQuestions(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [session]); const generateQuestions = async () => { if (!session) return; setError(null); try { const res = await fetch('/api/generate-questions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-gemini-api-key': localStorage.getItem('gemini-api-key') || '' }, body: JSON.stringify({ role: session.config.role, difficulty: session.config.difficulty, questionCount: session.config.questionCount, }), }); const data = await res.json(); if (!res.ok) throw new Error(data.error); const questions: Question[] = data.questions; setQuestions(questions); // Add welcome message addMessage({ id: 'welcome', role: 'ai', content: `Welcome to your **${session.config.role.title}** mock interview! 🎯\n\nI'll be asking you **${questions.length} questions** at **${session.config.difficulty}** difficulty.\n\nTake your time to think before answering. Let's begin!`, timestamp: Date.now(), }); // Add first question setTimeout(() => { addMessage({ id: `q-${questions[0].id}`, role: 'ai', content: `**Question 1/${questions.length}** • _${questions[0].category}_\n\n${questions[0].text}`, timestamp: Date.now(), questionId: questions[0].id, }); setIsLoading(false); }, 800); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to generate questions'); setIsLoading(false); } }; const handleSubmitAnswer = async () => { if (!session || !input.trim() || isEvaluating) return; const currentQ = session.questions[session.currentQuestionIndex]; if (!currentQ) return; const answerText = input.trim(); setInput(''); // Add user message addMessage({ id: `a-${currentQ.id}`, role: 'user', content: answerText, timestamp: Date.now(), questionId: currentQ.id, }); // Submit answer submitAnswer({ questionId: currentQ.id, text: answerText, timestamp: Date.now(), }); // Evaluate setIsEvaluating(true); try { const res = await fetch('/api/evaluate-answer', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-gemini-api-key': localStorage.getItem('gemini-api-key') || '' }, body: JSON.stringify({ question: currentQ.text, answer: answerText, role: session.config.role.title, difficulty: session.config.difficulty, }), }); const data = await res.json(); if (!res.ok) throw new Error(data.error); const evaluation: Evaluation = { ...data.evaluation, questionId: currentQ.id, }; addEvaluation(evaluation); setShowEvaluation(evaluation); // Add AI feedback message addMessage({ id: `eval-${currentQ.id}`, role: 'ai', content: buildEvaluationMessage(evaluation), timestamp: Date.now(), questionId: currentQ.id, evaluation, }); } catch (err) { addMessage({ id: `err-${currentQ.id}`, role: 'ai', content: `⚠️ I couldn't evaluate that response. ${err instanceof Error ? err.message : 'Please try again.'}`, timestamp: Date.now(), }); } setIsEvaluating(false); }; const handleNextQuestion = useCallback(() => { if (!session) return; setShowEvaluation(null); const nextIndex = session.currentQuestionIndex + 1; if (nextIndex >= session.questions.length) { // End of interview handleEndInterview(); return; } nextQuestion(); const nextQ = session.questions[nextIndex]; addMessage({ id: `q-${nextQ.id}`, role: 'ai', content: `**Question ${nextIndex + 1}/${session.questions.length}** • _${nextQ.category}_\n\n${nextQ.text}`, timestamp: Date.now(), questionId: nextQ.id, }); inputRef.current?.focus(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [session, nextQuestion, addMessage]); const handleEndInterview = async () => { if (!session) return; setIsGeneratingSummary(true); try { const res = await fetch('/api/session-summary', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-gemini-api-key': localStorage.getItem('gemini-api-key') || '' }, body: JSON.stringify({ role: session.config.role.title, difficulty: session.config.difficulty, evaluations: session.evaluations, questions: session.questions, answers: session.answers, }), }); const data = await res.json(); if (!res.ok) throw new Error(data.error); completeSession(data.summary); router.push('/dashboard'); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to generate summary'); setIsGeneratingSummary(false); } }; const handleSkipQuestion = () => { if (!session) return; const currentQ = session.questions[session.currentQuestionIndex]; addMessage({ id: `skip-${currentQ.id}`, role: 'user', content: '_Skipped this question_', timestamp: Date.now(), questionId: currentQ.id, }); submitAnswer({ questionId: currentQ.id, text: '[SKIPPED]', timestamp: Date.now(), }); handleNextQuestion(); }; // Voice input const toggleVoiceInput = () => { if (isListening) { recognitionRef.current?.stop(); setIsListening(false); return; } // eslint-disable-next-line @typescript-eslint/no-explicit-any const win = window as any; const SpeechRecognitionAPI = win.SpeechRecognition || win.webkitSpeechRecognition; if (!SpeechRecognitionAPI) { setError('Voice input is not supported in your browser.'); return; } const recognition = new SpeechRecognitionAPI(); recognition.continuous = true; recognition.interimResults = true; // eslint-disable-next-line @typescript-eslint/no-explicit-any recognition.onresult = (event: any) => { let transcript = ''; for (let i = 0; i < event.results.length; i++) { transcript += event.results[i][0].transcript; } setInput(transcript); }; recognition.onerror = () => { setIsListening(false); }; recognition.onend = () => { setIsListening(false); }; recognition.start(); recognitionRef.current = recognition; setIsListening(true); }; const formatTime = (seconds: number) => { const m = Math.floor(seconds / 60); const s = seconds % 60; return `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`; }; if (!session) return null; if (isLoading) { return ; } if (isGeneratingSummary) { return ; } const progress = session.questions.length > 0 ? ((session.currentQuestionIndex + (session.evaluations.length > session.currentQuestionIndex ? 1 : 0)) / session.questions.length) * 100 : 0; const timeLimitSeconds = session.config.timeLimit * 60; const remainingSeconds = session.config.timeLimit > 0 ? Math.max(0, timeLimitSeconds - elapsedTime) : 0; const isRunningOut = session.config.timeLimit > 0 && remainingSeconds <= 120; const displayTime = session.config.timeLimit > 0 ? remainingSeconds : elapsedTime; return (
{/* Top Bar */}
{session.config.role.icon}

{session.config.role.title}

{session.config.difficulty} • Q{session.currentQuestionIndex + 1}/{session.questions.length}

{/* Progress bar */}
{/* Timer */}
{formatTime(displayTime)}
{/* End button */}
{/* Error bar */} {error && (

{error}

)} {/* Messages */}
{session.messages.map((msg) => ( ))} {/* Evaluating indicator */} {isEvaluating && (
🤖
)}
{/* Evaluation Drawer */} {showEvaluation && (

Score: {showEvaluation.score}/10

{[ { label: 'Relevance', value: showEvaluation.relevance }, { label: 'Clarity', value: showEvaluation.clarity }, { label: 'Accuracy', value: showEvaluation.technicalAccuracy }, ].map((m) => ( {m.label}: = 7 ? 'var(--success)' : m.value >= 4 ? 'var(--warning)' : 'var(--danger)' }}>{m.value}/10 ))}
)}
{/* Input Area */} {!showEvaluation && session.status !== 'completed' && (