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);