diff --git a/app/Http/Controllers/InterviewController.php b/app/Http/Controllers/InterviewController.php index 9dc2460..b09391b 100644 --- a/app/Http/Controllers/InterviewController.php +++ b/app/Http/Controllers/InterviewController.php @@ -663,11 +663,11 @@ public function getPollData($id) } } - // Clean up stale active_peers (> 15 seconds) + // Clean up stale active_peers (> 30 seconds) $activePeers = $interview->active_peers ?? []; $now = now()->timestamp; $validPeers = array_values(array_filter($activePeers, function ($p) use ($now) { - return ($now - ($p['last_seen'] ?? 0)) < 15; + return ($now - ($p['last_seen'] ?? 0)) < 30; })); if (count($validPeers) !== count($activePeers)) { $interview->update(['active_peers' => $validPeers]); @@ -726,12 +726,13 @@ public function registerPeerHeartbeat(Request $request, $id) $role = $request->input('role', $user ? ($user->is_admin ? 'admin' : 'interviewer') : 'candidate'); $userId = $user ? $user->id : 0; + $interview->refresh(); $activePeers = $interview->active_peers ?? []; $now = now()->timestamp; $updatedPeers = []; foreach ($activePeers as $p) { - if (($now - ($p['last_seen'] ?? 0)) < 15) { + if (($now - ($p['last_seen'] ?? 0)) < 30) { if ($action === 'leave' && ($p['peer_id'] ?? '') === $peerId) { continue; } diff --git a/resources/js/candidate-proctor.js b/resources/js/candidate-proctor.js index 981a954..7830d76 100644 --- a/resources/js/candidate-proctor.js +++ b/resources/js/candidate-proctor.js @@ -1135,7 +1135,7 @@ class CandidateProctoring { const objectDetector = await this.withAmdLoaderDisabled(() => this.createModel(vision.ObjectDetector, fileset, { modelAssetPath: OBJECT_MODEL_URL, - runningMode: 'VIDEO', maxResults: 10, scoreThreshold: 0.15, + runningMode: 'VIDEO', maxResults: 10, scoreThreshold: 0.35, }) ); this.phoneDetector = new PhoneDetector(objectDetector); diff --git a/resources/js/candidate-room.js b/resources/js/candidate-room.js index e5991be..78a8177 100644 --- a/resources/js/candidate-room.js +++ b/resources/js/candidate-room.js @@ -271,55 +271,39 @@ export function saveCanvasDrawing(force = false) { } // --- Candidate Chat Polling & Sending --- -export function pollCandidateChat() { - const interviewId = document.body.dataset.interviewId; - if (!interviewId) return; +export function pollCandidateChat(data) { + if (!data) return; + const chatHistory = document.getElementById('candidate-chat-history'); + if (!chatHistory) return; - fetch(`/candidate/poll/${interviewId}`, { - headers: { 'Accept': 'application/json' } - }) - .then(r => { - if (!r.ok) return null; - const contentType = r.headers.get('content-type'); - if (!contentType || !contentType.includes('application/json')) return null; - return r.json(); - }) - .then(data => { - if (!data) return; - const chatHistory = document.getElementById('candidate-chat-history'); - if (!chatHistory) return; + if (data.status === 'blocked' || data.is_expired) { + return; + } - if (data.status === 'blocked' || data.is_expired) { - window.location.reload(); + if (data.warnings && Array.isArray(data.warnings)) { + if (data.warnings.length === 0) { + chatHistory.innerHTML = '
No messages yet. Send a message below.
'; return; } - if (data.warnings && Array.isArray(data.warnings)) { - if (data.warnings.length === 0) { - chatHistory.innerHTML = '
No messages yet. Send a message below.
'; - return; - } + let html = ''; + data.warnings.forEach(w => { + const isInterviewer = w.sent_by !== null; + const senderName = isInterviewer ? (w.sender ? w.sender.name : 'Interviewer / Panelist') : 'You (Candidate)'; + const badgeColor = isInterviewer ? '#6366f1' : '#38bdf8'; + const bgColor = isInterviewer ? 'rgba(99,102,241,0.15)' : 'rgba(56,189,248,0.15)'; - let html = ''; - data.warnings.forEach(w => { - const isInterviewer = w.sent_by !== null; - const senderName = isInterviewer ? (w.sender ? w.sender.name : 'Interviewer / Panelist') : 'You (Candidate)'; - const badgeColor = isInterviewer ? '#6366f1' : '#38bdf8'; - const bgColor = isInterviewer ? 'rgba(99,102,241,0.15)' : 'rgba(56,189,248,0.15)'; - - html += `
-
- ${senderName} - ${new Date(w.created_at || Date.now()).toLocaleTimeString()} -
-
${w.message}
-
`; - }); - chatHistory.innerHTML = html; - chatHistory.scrollTop = chatHistory.scrollHeight; - } - }) - .catch(() => {}); + html += `
+
+ ${senderName} + ${new Date(w.created_at || Date.now()).toLocaleTimeString()} +
+
${w.message}
+
`; + }); + chatHistory.innerHTML = html; + chatHistory.scrollTop = chatHistory.scrollHeight; + } } export function sendCandidateMessage() { @@ -345,7 +329,13 @@ export function sendCandidateMessage() { }) .then(r => r.json()) .then(() => { - pollCandidateChat(); + fetch(`/candidate/poll/${interviewId}`, { + headers: { 'Accept': 'application/json' } + }) + .then(r => r.json()) + .then(data => { + if (data) pollCandidateChat(data); + }); }); } @@ -972,10 +962,6 @@ export function initCandidateRoom() { // Start Candidate Camera startCandidateCameraStream(); - // Chat polling - setInterval(pollCandidateChat, 600); - pollCandidateChat(); - // Heartbeat & Polling if (candidateHeartbeatIntervalTimer) clearInterval(candidateHeartbeatIntervalTimer); sendCandidatePeerHeartbeat(); @@ -985,7 +971,6 @@ export function initCandidateRoom() { setInterval(() => { if (!interviewId) return; - sendCandidatePeerHeartbeat('heartbeat'); fetch('/candidate/poll/' + interviewId, { headers: { 'Accept': 'application/json' } @@ -994,6 +979,8 @@ export function initCandidateRoom() { .then(data => { if (!data) return; + pollCandidateChat(data); + if (data.active_peers && Array.isArray(data.active_peers)) { const activePeerIds = new Set(data.active_peers.map(p => p.peer_id)); diff --git a/resources/js/interview-call.js b/resources/js/interview-call.js index 108a0ea..a6e8072 100644 --- a/resources/js/interview-call.js +++ b/resources/js/interview-call.js @@ -606,7 +606,7 @@ export function updateCandidateStatusIcons(isMicOn, isCamOn, isJoined = true) { const screenWrapper = document.getElementById('screen-wrapper'); const joinedPill = document.getElementById('candidate-joined-pill'); - if (!isJoined) { + if (!isJoined && !candidateStreamConnected) { if (joinedPill) { joinedPill.className = 'pill gray inline-flex items-center gap-1.5 font-semibold text-[11px] px-2.5 py-0.5 rounded-full border border-slate-700 text-slate-400 bg-slate-800/60'; joinedPill.innerHTML = ' Not Joined'; @@ -692,13 +692,18 @@ export function pollReviewData() { const currentUserId = window.currentUserId || 0; const myPeerId = 'interviewer_' + currentInterviewId + '_' + currentUserId; + const candVid = document.getElementById('interviewer-cand-video'); + const candidateStreamConnected = candVid && candVid.srcObject && (candVid.srcObject.active || (candVid.srcObject.getTracks && candVid.srcObject.getTracks().length > 0)); + const candidatePeer = (data.active_peers && Array.isArray(data.active_peers)) ? data.active_peers.find(p => p.role === 'candidate' || (p.peer_id && p.peer_id.startsWith('cand_'))) : null; - if (candidatePeer) { - const isMicOn = isTrueVal(candidatePeer.mic_on); - const isCamOn = isTrueVal(candidatePeer.cam_on); + const isCandidateJoined = Boolean(candidatePeer) || Boolean(candidateStreamConnected); + + if (isCandidateJoined) { + const isMicOn = candidatePeer ? isTrueVal(candidatePeer.mic_on) : true; + const isCamOn = candidatePeer ? isTrueVal(candidatePeer.cam_on) : true; updateCandidateStatusIcons(isMicOn, isCamOn, true); } else { updateCandidateStatusIcons(false, false, false);