orderBy('created_at', 'desc') ->get(); $interviewers = User::orderBy('name', 'asc')->get(); return view('interview.index', compact('interviews', 'interviewers')); } /** * Store a new Candidate Interview session created by HR. */ public function store(Request $request) { $request->validate([ 'candidate_name' => 'required|string|max:255', 'candidate_email' => 'required|email|max:255', 'candidate_phone' => 'required|string|max:50', 'valid_hours' => 'required|integer|min:1|max:72', 'language' => 'required|string', 'assigned_interviewers' => 'nullable|array', ]); $tempPassword = 'Pass-' . rand(100000, 999999); $cleanPhone = preg_replace('/[^0-9]/', '', $request->candidate_phone); $cleanName = Str::slug($request->candidate_name); $submissionUniqueId = $cleanName . '_' . $cleanPhone . '_' . time(); $expiresAt = now()->addHours((int)$request->valid_hours); $interview = Interview::create([ 'candidate_name' => $request->candidate_name, 'candidate_email' => strtolower(trim($request->candidate_email)), 'candidate_phone' => $request->candidate_phone, 'temp_password' => $tempPassword, 'expires_at' => $expiresAt, 'language' => $request->language, 'status' => 'scheduled', 'assigned_interviewers' => $request->assigned_interviewers ?? [], 'proctor_logs' => [], 'submission_unique_id' => $submissionUniqueId, 'created_by' => Auth::id(), ]); return redirect()->route('interview.index')->with('success', 'Interview session created for ' . $request->candidate_name . '. Temporary login credentials generated!'); } /** * Show Candidate Login Screen. */ public function showCandidateLogin() { return view('interview.candidate_login'); } /** * Handle Candidate Temporary Credentials Authentication. */ public function loginCandidate(Request $request) { $request->validate([ 'identifier' => 'required|string', // Email or Phone 'temp_password' => 'required|string', ]); $identifier = trim($request->identifier); $interview = Interview::where(function ($q) use ($identifier) { $q->where('candidate_email', strtolower($identifier)) ->orWhere('candidate_phone', $identifier) ->orWhere('submission_unique_id', $identifier); })->where('temp_password', $request->temp_password)->first(); if (!$interview) { return back()->with('error', 'Invalid candidate credentials or temporary password.'); } if ($interview->isExpired()) { $interview->update(['status' => 'expired']); return back()->with('error', 'This candidate interview access window has expired. Please contact HR.'); } // Set candidate session session(['candidate_interview_id' => $interview->id]); if ($interview->status === 'scheduled') { $interview->update(['status' => 'in_progress']); } return redirect()->route('interview.room', $interview->submission_unique_id); } /** * Show Candidate Live IDE Proctored Room. */ public function showCandidateRoom($uniqueId) { $interview = Interview::where('submission_unique_id', $uniqueId)->firstOrFail(); if ($interview->isExpired()) { $interview->update(['status' => 'expired']); return response()->view('interview.expired', compact('interview'), 403); } return view('interview.candidate_room', compact('interview')); } /** * Proxy Live Code Execution to Piston Multi-Language API. */ public function executeCode(Request $request) { $request->validate([ 'language' => 'required|string', 'code' => 'required|string', ]); $langMap = [ 'python' => ['language' => 'python', 'version' => '3.10.0'], 'c' => ['language' => 'c', 'version' => '10.2.0'], 'cpp' => ['language' => 'c++', 'version' => '10.2.0'], 'java' => ['language' => 'java', 'version' => '15.0.2'], 'php' => ['language' => 'php', 'version' => '8.2.3'], 'laravel' => ['language' => 'php', 'version' => '8.2.3'], 'javascript' => ['language' => 'javascript', 'version' => '18.15.0'], ]; $targetLang = $langMap[strtolower($request->language)] ?? ['language' => 'python', 'version' => '3.10.0']; try { $response = Http::post('https://emkc.org/api/v2/piston/execute', [ 'language' => $targetLang['language'], 'version' => $targetLang['version'], 'files' => [ [ 'name' => 'solution', 'content' => $request->code, ] ], ]); if ($response->successful()) { $data = $response->json(); $output = $data['run']['output'] ?? 'No output generated.'; return response()->json(['success' => true, 'output' => $output]); } return response()->json(['success' => false, 'output' => 'Execution engine error: ' . $response->status()]); } catch (\Exception $e) { return response()->json(['success' => false, 'output' => 'Code runner service error: ' . $e->getMessage()]); } } /** * Save Candidate Submitted Code & Execution Results. */ public function submitCode(Request $request, $id) { $interview = Interview::findOrFail($id); $request->validate([ 'code' => 'required|string', 'language' => 'required|string', 'output' => 'nullable|string', ]); $interview->update([ 'submitted_code' => $request->code, 'submitted_language' => $request->language, 'code_output' => $request->output, 'status' => 'completed', ]); return response()->json([ 'success' => true, 'message' => 'Code submitted successfully under unique ID: ' . $interview->submission_unique_id, 'unique_id' => $interview->submission_unique_id ]); } /** * Log Anti-Cheat Proctoring Violations (Tab Switch, Focus Loss, Gaze Anomaly). */ public function logViolation(Request $request, $id) { $interview = Interview::findOrFail($id); $type = $request->input('type'); // tab_switch, focus_lost, paste_event, gaze_anomaly $details = $request->input('details', ''); $logs = $interview->proctor_logs ?? []; $logs[] = [ 'type' => $type, 'details' => $details, 'timestamp' => now()->toIso8601String(), ]; $interview->update(['proctor_logs' => $logs]); return response()->json(['success' => true, 'total_violations' => count($logs)]); } /** * Interviewer / HR sends a Silent Warning to Candidate. */ public function sendWarning(Request $request, $id) { $interview = Interview::findOrFail($id); $request->validate([ 'message' => 'required|string|max:500', ]); $warning = InterviewWarning::create([ 'interview_id' => $interview->id, 'message' => $request->message, 'sent_by' => Auth::id(), ]); return response()->json(['success' => true, 'warning' => $warning]); } /** * Live Polling Endpoint for Candidate Warnings & HR Monitoring. */ public function getPollData($id) { $interview = Interview::with(['warnings.sender'])->findOrFail($id); return response()->json([ 'status' => $interview->status, 'submitted_code' => $interview->submitted_code, 'submitted_language' => $interview->submitted_language, 'code_output' => $interview->code_output, 'proctor_logs' => $interview->proctor_logs ?? [], 'warnings' => $interview->warnings, 'is_expired' => $interview->isExpired(), ]); } /** * Save Candidate Silent Video Recording Blob / Snapshot Stream. */ public function uploadRecording(Request $request, $id) { $interview = Interview::findOrFail($id); if ($request->hasFile('video')) { $file = $request->file('video'); $filename = 'interview_' . $interview->submission_unique_id . '_' . time() . '.webm'; $path = $file->storeAs('public/recordings', $filename); $interview->update(['recording_path' => Storage::url($path)]); return response()->json(['success' => true, 'path' => Storage::url($path)]); } return response()->json(['success' => false, 'message' => 'No video payload received']); } }