From c67ee1fceb5ddcfdc05d970f17fb83459f88a6ad Mon Sep 17 00:00:00 2001 From: subhajit Date: Fri, 31 Jul 2026 12:24:52 +0530 Subject: [PATCH 1/3] update --- app/Http/Controllers/InterviewController.php | 18 +++++++++- resources/views/interview/index.blade.php | 35 ++++++++++++++------ 2 files changed, 42 insertions(+), 11 deletions(-) diff --git a/app/Http/Controllers/InterviewController.php b/app/Http/Controllers/InterviewController.php index a5cd4ef..45ca34d 100644 --- a/app/Http/Controllers/InterviewController.php +++ b/app/Http/Controllers/InterviewController.php @@ -533,6 +533,14 @@ public function getActiveCalls(Request $request) $activeCalls = []; foreach ($accessible as $interview) { + // Auto-terminate call session if interview timeline has expired + if ($interview->isExpired()) { + if ($interview->call_status === 'active') { + $interview->update(['call_status' => 'ended']); + } + continue; + } + if ($interview->call_status === 'active') { $starterName = 'Panelist'; if ($interview->call_started_by) { @@ -570,6 +578,14 @@ public function getPollData($id) ->where('id', $id) ->orWhere('submission_unique_id', $id) ->firstOrFail(); + + // Enforce expiration: ended call status if timeline is expired + if ($interview->isExpired()) { + if ($interview->call_status === 'active') { + $interview->update(['call_status' => 'ended']); + } + } + $formattedRecordings = $this->getFormattedRecordings($interview); $starterName = $interview->callStarter ? $interview->callStarter->name : null; @@ -580,7 +596,7 @@ public function getPollData($id) return response()->json([ 'status' => $interview->status, - 'call_status' => $interview->call_status ?? 'idle', + 'call_status' => $interview->isExpired() ? 'ended' : ($interview->call_status ?? 'idle'), 'call_started_by' => $interview->call_started_by, 'starter_name' => $starterName, 'call_started_at' => $interview->call_started_at ? $interview->call_started_at->toIso8601String() : null, diff --git a/resources/views/interview/index.blade.php b/resources/views/interview/index.blade.php index 361e7e0..80d5350 100644 --- a/resources/views/interview/index.blade.php +++ b/resources/views/interview/index.blade.php @@ -1029,17 +1029,24 @@ class CallRingtoneEngine { const endedCallIds = new Set(); const currentUserId = {{ Auth::id() }}; const globalCallChannel = new BroadcastChannel('global_call_alerts'); + const adminPageLoadTimestamp = Date.now(); - function triggerIncomingCallRing(callData) { + function triggerIncomingCallRing(callData, isRealtimeBroadcast = false) { if (endedCallIds.has(callData.id) || dismissedCallIds.has(callData.id)) return; if (iAmTheCaller && currentInterviewId == callData.id) return; if (interviewerLocalStream && currentInterviewId == callData.id) return; - // Check ring 10-second threshold based on call_started_at + // Only play ringtone for real-time fresh calls initiated within the last 8 seconds if (callData.call_started_at) { const startedTime = new Date(callData.call_started_at).getTime(); const elapsedMs = Date.now() - startedTime; - if (elapsedMs > 10000) return; + if (elapsedMs > 8000 && !isRealtimeBroadcast) { + dismissedCallIds.add(callData.id); + return; + } + } else if (!isRealtimeBroadcast) { + dismissedCallIds.add(callData.id); + return; } activeIncomingCall = callData; @@ -1048,13 +1055,13 @@ function triggerIncomingCallRing(callData) { document.getElementById('incoming-call-modal').classList.add('active'); callRingtone.start(); - // Set 10-second auto-stop ring timeout + // Set 8-second auto-stop ring timeout if (ringTimeoutTimer) clearTimeout(ringTimeoutTimer); - let remainingMs = 10000; + let remainingMs = 8000; if (callData.call_started_at) { const startedTime = new Date(callData.call_started_at).getTime(); const elapsedMs = Date.now() - startedTime; - remainingMs = Math.max(1000, 10000 - elapsedMs); + remainingMs = Math.max(1000, 8000 - elapsedMs); } ringTimeoutTimer = setTimeout(() => { @@ -1073,7 +1080,7 @@ function triggerIncomingCallRing(callData) { submission_unique_id: data.submission_unique_id, starter_name: data.starter_name, call_started_at: data.call_started_at || new Date().toISOString() - }); + }, true); } else if (data.type === 'call_ended') { endedCallIds.add(data.interview_id); dismissedCallIds.add(data.interview_id); @@ -1111,8 +1118,16 @@ function pollGlobalActiveCalls() { }); if (incoming) { + // Check if call was started before page refresh + const startedTime = incoming.call_started_at ? new Date(incoming.call_started_at).getTime() : 0; + if (startedTime > 0 && startedTime < (adminPageLoadTimestamp - 4000)) { + // Ongoing call from before page reload: silent status update without ringtone + dismissedCallIds.add(incoming.id); + return; + } + if (!activeIncomingCall || activeIncomingCall.id !== incoming.id) { - triggerIncomingCallRing(incoming); + triggerIncomingCallRing(incoming, false); } } else if (activeIncomingCall) { declineIncomingCall(true); @@ -1121,8 +1136,8 @@ function pollGlobalActiveCalls() { .catch(() => {}); } - // Poll for active calls every 1 second for instant ring notification - setInterval(pollGlobalActiveCalls, 1000); + // Poll for active calls every 1.5 seconds + setInterval(pollGlobalActiveCalls, 1500); function acceptIncomingCall() { callRingtone.stop(); From 367771772fd1117e21b11a84b5e6ddbcf356bacf Mon Sep 17 00:00:00 2001 From: subhajit Date: Fri, 31 Jul 2026 15:24:24 +0530 Subject: [PATCH 2/3] update --- resources/views/admin/dashboard.blade.php | 108 +------------ .../views/interview/candidate_room.blade.php | 91 +++++++---- resources/views/interview/index.blade.php | 147 +++++++++++++++--- 3 files changed, 189 insertions(+), 157 deletions(-) diff --git a/resources/views/admin/dashboard.blade.php b/resources/views/admin/dashboard.blade.php index 22b76af..c0fdc82 100644 --- a/resources/views/admin/dashboard.blade.php +++ b/resources/views/admin/dashboard.blade.php @@ -577,12 +577,15 @@ .badge { display: inline-flex; align-items: center; + justify-content: center; + gap: 4px; padding: 4px 8px; border-radius: 9999px; font-size: 0.7rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; + white-space: nowrap; } .badge-admin { @@ -910,76 +913,7 @@ @endif - -
-
-
-

- ⚙️ Global Admin Proctoring & System Settings -

-
Master control panel for candidate assessment rules, system screen capturing, and module toggles
-
- - 🛡️ ADMIN CONTROL PANEL - -
-
- - -
-
- @csrf -
-
- 📸 Candidate System Screenshot Capture -
-
- Captures candidate system desktop screen automatically on call start & tab switches. Admin can stop taking screenshots anytime across all candidates. -
-
-
- - {{ \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1' ? '🟢 ENABLED (Active)' : '🔴 STOPPED / DISABLED' }} - - -
-
-
- - -
-
- @csrf -
-
- 🚀 Candidate Provisioning & Onboarding Module -
-
- Master toggle to enable/disable automated candidate provisioning, onboarding checklists, and 1-click revocation. -
-
-
- - {{ \App\Models\Setting::get('onboarding_enabled', '1') === '1' ? '🟢 ENABLED' : '🔴 DISABLED' }} - - -
-
-
- -
-
@@ -1241,41 +1175,7 @@ - - - - -
- @csrf -
-
-
🚀 Onboarding & Offboarding Module
-
Enable/disable candidate provisioning & 1-click revocation
-
- -
-
- - -
- @csrf -
-
-
📸 Candidate System Screenshot Capture
-
Master Admin control to stop/start candidate system screen capturing (call start & tab switch)
-
- -
+
diff --git a/resources/views/interview/candidate_room.blade.php b/resources/views/interview/candidate_room.blade.php index d173812..edb8b51 100644 --- a/resources/views/interview/candidate_room.blade.php +++ b/resources/views/interview/candidate_room.blade.php @@ -458,13 +458,20 @@ } }); - // Real-Time Code Sync to Interviewer (Before Submission) + // Real-Time Instant BroadcastChannel + Backend Code Sync + const liveSyncChannel = new BroadcastChannel('live_sync_' + '{{ $interview->id }}'); let codeSyncTimeout = null; editor.onDidChangeModelContent(function () { + const code = editor.getValue(); + const lang = document.getElementById('language-select').value; + + // Broadcast instant local event (0ms latency) + if (typeof liveSyncChannel !== 'undefined' && liveSyncChannel) { + liveSyncChannel.postMessage({ type: 'code_sync', code: code, language: lang }); + } + clearTimeout(codeSyncTimeout); codeSyncTimeout = setTimeout(function () { - const code = editor.getValue(); - const lang = document.getElementById('language-select').value; fetch(`{{ route("interview.candidate.sync-code", $interview->id) }}`, { method: 'POST', headers: { @@ -473,7 +480,7 @@ }, body: JSON.stringify({ code: code, language: lang }) }); - }, 600); + }, 200); }); }); @@ -1176,6 +1183,10 @@ function showCandidateJoinOption() { if (joinBtn) joinBtn.style.display = 'none'; + if (!localMediaStream) { + await startCandidateCameraStream(); + } + isCandidateCallConnected = true; const badge = document.getElementById('call-status-badge'); @@ -1380,6 +1391,12 @@ function initCandidatePeer() { peerInstance.on('call', call => { activeCallInstance = call; + latestCallStatus = 'active'; + + if (!isCandidateCallConnected && !candidateDeclinedOrLeftCall) { + triggerCandidateRing(); + } + const sendStream = localMediaStream || new MediaStream(); call.answer(sendStream); @@ -1388,15 +1405,17 @@ function initCandidatePeer() { const placeholder = document.getElementById('interviewer-video-placeholder'); if (remoteVideo) { remoteVideo.srcObject = remoteStream; - remoteVideo.play().catch(e => console.log(e)); + remoteVideo.play().catch(e => console.log('Candidate remote video play error:', e)); } if (placeholder) placeholder.style.display = 'none'; - const badge = document.getElementById('call-status-badge'); - if (badge) { - badge.innerText = '🟢 CALL CONNECTED (LIVE)'; - badge.style.background = 'rgba(16,185,129,0.3)'; - badge.style.color = '#34d399'; + if (isCandidateCallConnected) { + const badge = document.getElementById('call-status-badge'); + if (badge) { + badge.innerText = '🟢 CALL CONNECTED (LIVE)'; + badge.style.background = 'rgba(16,185,129,0.3)'; + badge.style.color = '#34d399'; + } } }); @@ -1830,9 +1849,13 @@ function toggleNotepadModal() { } function saveNotepadContent() { + const notes = document.getElementById('notepad-textarea').value; + if (typeof liveSyncChannel !== 'undefined' && liveSyncChannel) { + liveSyncChannel.postMessage({ type: 'notes_sync', notes: notes }); + } + clearTimeout(notesTimeout); notesTimeout = setTimeout(() => { - const notes = document.getElementById('notepad-textarea').value; fetch(`{{ route('interview.candidate.notes', $interview->id) }}`, { method: 'POST', headers: { @@ -1841,13 +1864,14 @@ function saveNotepadContent() { }, body: JSON.stringify({ notes: notes }) }); - }, 300); + }, 150); } // HTML5 Canvas Drawing Logic let isDrawing = false; let canvas, ctx; let drawingSaveTimeout = null; + let lastDrawingSaveTime = 0; function toggleDrawingModal() { const modal = document.getElementById('drawing-modal'); @@ -1889,29 +1913,38 @@ function draw(e) { ctx.strokeStyle = document.getElementById('draw-color').value; ctx.lineTo(e.offsetX, e.offsetY); ctx.stroke(); - saveCanvasDrawing(); + saveCanvasDrawing(false); } function stopDraw() { if (isDrawing) { isDrawing = false; ctx.closePath(); - saveCanvasDrawing(); + saveCanvasDrawing(true); } } function clearCanvasDraw() { if (ctx && canvas) { ctx.clearRect(0, 0, canvas.width, canvas.height); - saveCanvasDrawing(); + saveCanvasDrawing(true); } } - function saveCanvasDrawing() { - clearTimeout(drawingSaveTimeout); - drawingSaveTimeout = setTimeout(() => { - if (!canvas) return; - const drawingData = canvas.toDataURL(); + function saveCanvasDrawing(force = false) { + if (!canvas) return; + const now = Date.now(); + const drawingData = canvas.toDataURL(); + + // Broadcast instant local event (0ms latency) + if (typeof liveSyncChannel !== 'undefined' && liveSyncChannel) { + liveSyncChannel.postMessage({ type: 'drawing_sync', drawing: drawingData }); + } + + // Throttled HTTP POST save to backend (fires every 250ms while drawing & on mouseup) + if (force || (now - lastDrawingSaveTime >= 250)) { + clearTimeout(drawingSaveTimeout); + lastDrawingSaveTime = now; fetch(`{{ route('interview.candidate.drawing', $interview->id) }}`, { method: 'POST', headers: { @@ -1920,7 +1953,12 @@ function saveCanvasDrawing() { }, body: JSON.stringify({ drawing: drawingData }) }); - }, 300); + } else { + clearTimeout(drawingSaveTimeout); + drawingSaveTimeout = setTimeout(() => { + saveCanvasDrawing(true); + }, 250); + } } setInterval(() => { @@ -1944,18 +1982,13 @@ function saveCanvasDrawing() { }, 1200); } if (!isCandidateCallConnected && !candidateDeclinedOrLeftCall) { - const elapsed = data.call_started_at ? (Date.now() - new Date(data.call_started_at).getTime()) : 999999; - if (elapsed <= 12000 && lastRungCallStartedAt !== data.call_started_at) { - lastRungCallStartedAt = data.call_started_at; - triggerCandidateRing(null, data.call_started_at); - } else if (elapsed > 12000) { - showCandidateJoinOption(); - } + triggerCandidateRing(null, data.call_started_at); } } else if (data.call_status === 'ended' || data.call_status === 'idle') { latestCallStatus = data.call_status; hasCapturedCallStartScreenshot = false; lastRungCallStartedAt = null; + candidateDeclinedOrLeftCall = false; candRingtone.stop(); if (candRingTimer) clearTimeout(candRingTimer); isCandidateCallConnected = false; @@ -1978,7 +2011,7 @@ function saveCanvasDrawing() { } }) .catch(() => {}); - }, 2200); + }, 1000); diff --git a/resources/views/interview/index.blade.php b/resources/views/interview/index.blade.php index 80d5350..c8830fe 100644 --- a/resources/views/interview/index.blade.php +++ b/resources/views/interview/index.blade.php @@ -128,10 +128,14 @@ .badge { display: inline-flex; align-items: center; - padding: 4px 8px; + justify-content: center; + gap: 5px; + padding: 4px 10px; border-radius: 9999px; font-size: 0.7rem; font-weight: 600; + white-space: nowrap; + line-height: 1.2; } .admin-modal { @@ -215,32 +219,80 @@ {{ session('success') }} @endif - -
-
-
📸
+ +
+
-
Global Candidate System Screenshot Control
-
- Captures candidate desktop screen on call start & tab switches. - Admin can stop/start screen capturing globally anytime. -
+

+ ⚙️ Global Admin Proctoring & System Settings +

+
Master control panel for candidate assessment rules, system screen capturing, and module toggles
-
-
- - {{ \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1' ? '🟢 ENABLED (Screen Capturing Active)' : '🔴 STOPPED / DISABLED' }} + + 🛡️ ADMIN CONTROL PANEL - @if(Auth::user() && Auth::user()->isAdmin()) -
- @csrf - -
- @endif
-
+ +
+ + +
+
+ @csrf +
+
+ 📸 Candidate System Screenshot Capture +
+
+ Captures candidate system desktop screen automatically on call start & tab switches. Admin can stop taking screenshots anytime across all candidates. +
+
+
+ + {{ \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1' ? '🟢 ENABLED (Active)' : '🔴 STOPPED / DISABLED' }} + + @if(Auth::user() && Auth::user()->isAdmin()) + + @endif +
+
+
+ + +
+
+ @csrf +
+
+ 🚀 Candidate Provisioning & Onboarding Module +
+
+ Master toggle to enable/disable automated candidate provisioning, onboarding checklists, and 1-click revocation. +
+
+
+ + {{ \App\Models\Setting::get('onboarding_enabled', '1') === '1' ? '🟢 ENABLED' : '🔴 DISABLED' }} + + @if(Auth::user() && Auth::user()->isAdmin()) + + @endif +
+
+
+ +
+
@@ -1180,6 +1232,48 @@ function declineIncomingCall(silent = false) { } } + let liveSyncChannel = null; + + function subscribeLiveSync(interviewId) { + if (liveSyncChannel) { + try { liveSyncChannel.close(); } catch(e) {} + } + liveSyncChannel = new BroadcastChannel('live_sync_' + interviewId); + liveSyncChannel.onmessage = function(event) { + const data = event.data; + if (!data) return; + + if (data.type === 'code_sync') { + if (data.language) { + const langEl = document.getElementById('modal-code-lang'); + if (langEl) langEl.innerText = data.language.toUpperCase(); + } + if (data.code !== undefined) { + const codeEl = document.getElementById('modal-code-display'); + if (codeEl) codeEl.innerText = data.code || '// Candidate has not typed any code yet.'; + } + } else if (data.type === 'notes_sync') { + if (data.notes !== undefined) { + const notesEl = document.getElementById('modal-notes-display'); + if (notesEl) notesEl.innerText = data.notes || 'No candidate notes written yet.'; + } + } else if (data.type === 'drawing_sync') { + const drawingImg = document.getElementById('modal-drawing-img'); + const noDrawing = document.getElementById('no-drawing-placeholder'); + if (data.drawing) { + if (drawingImg) { + drawingImg.src = data.drawing; + drawingImg.style.display = 'block'; + } + if (noDrawing) noDrawing.style.display = 'none'; + } else { + if (drawingImg) drawingImg.style.display = 'none'; + if (noDrawing) noDrawing.style.display = 'block'; + } + } + }; + } + function openReviewModal(id, name, uid, autoJoinCall = false) { currentInterviewId = id; acknowledgedViolationCount = 0; @@ -1199,9 +1293,10 @@ function openReviewModal(id, name, uid, autoJoinCall = false) { initInterviewerPeer(); switchIdeTab('code'); + subscribeLiveSync(id); pollReviewData(); clearInterval(sosPollInterval); - sosPollInterval = setInterval(pollReviewData, 1200); + sosPollInterval = setInterval(pollReviewData, 500); if (autoJoinCall) { setTimeout(() => { @@ -2135,6 +2230,10 @@ function endInterviewerCall() { function closeReviewModal() { clearInterval(sosPollInterval); + if (liveSyncChannel) { + try { liveSyncChannel.close(); } catch(e) {} + liveSyncChannel = null; + } leaveInterviewerCall(true); const modal = document.getElementById('review-modal'); if (modal) modal.classList.remove('active'); From e3e1e8aaf5445a4abf2a73d3023dbae203e83ba5 Mon Sep 17 00:00:00 2001 From: subhajit Date: Fri, 31 Jul 2026 18:07:02 +0530 Subject: [PATCH 3/3] fixx bug --- app/Http/Controllers/InterviewController.php | 118 +++++++++--------- .../views/interview/candidate_room.blade.php | 15 ++- resources/views/interview/index.blade.php | 12 +- 3 files changed, 79 insertions(+), 66 deletions(-) diff --git a/app/Http/Controllers/InterviewController.php b/app/Http/Controllers/InterviewController.php index 45ca34d..e7eb740 100644 --- a/app/Http/Controllers/InterviewController.php +++ b/app/Http/Controllers/InterviewController.php @@ -155,69 +155,71 @@ public function executeCode(Request $request) $code = $request->code; $output = ''; - // 1. Instant Local CLI Process Execution (0.02s response for Node.js, Python, PHP) - try { - if (in_array($langKey, ['javascript', 'js'])) { - $process = new \Symfony\Component\Process\Process(['node', '-e', $code]); - $process->setTimeout(4); - $process->run(); - $out = $process->getOutput() ?: $process->getErrorOutput(); - $output = trim($out) ?: 'JavaScript executed successfully (no stdout).'; - } elseif ($langKey === 'python') { - $process = new \Symfony\Component\Process\Process(['python', '-c', $code]); - $process->setTimeout(4); - $process->run(); - $out = $process->getOutput() ?: $process->getErrorOutput(); - $output = trim($out) ?: 'Python executed successfully (no stdout).'; - } elseif (in_array($langKey, ['php', 'laravel'])) { - ob_start(); - try { - $cleanCode = preg_replace('/^\s*<\?php\s*/i', '', $code); - eval($cleanCode); - $out = ob_get_clean(); - $output = trim($out) ?: 'PHP code executed successfully.'; - } catch (\Throwable $e) { - ob_end_clean(); - $output = 'PHP Evaluation Error: ' . $e->getMessage(); + // 1. Ultra-Fast Piston Code Execution API (sub-second for C, C++, Java, JS, Python, PHP, Go, Rust) + $pistonLangMap = [ + 'python' => 'python', + 'py' => 'python', + 'javascript' => 'javascript', + 'js' => 'javascript', + 'c' => 'c', + 'cpp' => 'c++', + 'c++' => 'c++', + 'java' => 'java', + 'php' => 'php', + 'laravel' => 'php', + 'go' => 'go', + 'rust' => 'rust', + ]; + + if (isset($pistonLangMap[$langKey])) { + try { + $pistonLang = $pistonLangMap[$langKey]; + $response = Http::timeout(4)->post('https://emkc.org/api/v2/piston/execute', [ + 'language' => $pistonLang, + 'version' => '*', + 'files' => [ + ['content' => $code] + ] + ]); + + if ($response->successful()) { + $resData = $response->json(); + $output = $resData['run']['output'] ?? ($resData['run']['stdout'] ?? ''); + if (empty($output) && !empty($resData['run']['stderr'])) { + $output = $resData['run']['stderr']; + } } - } - } catch (\Exception $e) { - // Local execution exception; fall through to Judge0 API + } catch (\Exception $e) {} } - // 2. Remote Compiler API (Judge0 CE) for C, C++, Java or non-local runtimes + // 2. Instant Local Process Runner Fallback if (empty($output)) { - $judge0Map = [ - 'python' => 71, - 'c' => 50, - 'cpp' => 54, - 'c++' => 54, - 'java' => 62, - 'php' => 68, - 'laravel' => 68, - 'javascript' => 63, - 'js' => 63, - ]; - - if (isset($judge0Map[$langKey])) { - try { - $response = Http::timeout(5)->post('https://ce.judge0.com/submissions?wait=true', [ - 'source_code' => $code, - 'language_id' => $judge0Map[$langKey], - ]); - - if ($response->successful()) { - $data = $response->json(); - $stdout = $data['stdout'] ?? ''; - $stderr = $data['stderr'] ?? ''; - $compile = $data['compile_output'] ?? ''; - $msg = $data['message'] ?? ''; - - $outputParts = array_filter([$stdout, $stderr, $compile, $msg]); - $output = trim(implode("\n", $outputParts)); + try { + if (in_array($langKey, ['javascript', 'js'])) { + $process = new \Symfony\Component\Process\Process(['node', '-e', $code]); + $process->setTimeout(3); + $process->run(); + $out = $process->getOutput() ?: $process->getErrorOutput(); + $output = trim($out) ?: 'JavaScript executed successfully (no stdout).'; + } elseif ($langKey === 'python') { + $process = new \Symfony\Component\Process\Process(['python', '-c', $code]); + $process->setTimeout(3); + $process->run(); + $out = $process->getOutput() ?: $process->getErrorOutput(); + $output = trim($out) ?: 'Python executed successfully (no stdout).'; + } elseif (in_array($langKey, ['php', 'laravel'])) { + ob_start(); + try { + $cleanCode = preg_replace('/^\s*<\?php\s*/i', '', $code); + eval($cleanCode); + $out = ob_get_clean(); + $output = trim($out) ?: 'PHP code executed successfully.'; + } catch (\Throwable $e) { + ob_end_clean(); + $output = 'PHP Evaluation Error: ' . $e->getMessage(); } - } catch (\Exception $e) {} - } + } + } catch (\Exception $e) {} } if (empty($output)) { diff --git a/resources/views/interview/candidate_room.blade.php b/resources/views/interview/candidate_room.blade.php index edb8b51..3b552b4 100644 --- a/resources/views/interview/candidate_room.blade.php +++ b/resources/views/interview/candidate_room.blade.php @@ -368,7 +368,7 @@
No messages yet. Send a message below.
- + @@ -460,6 +460,11 @@ // Real-Time Instant BroadcastChannel + Backend Code Sync const liveSyncChannel = new BroadcastChannel('live_sync_' + '{{ $interview->id }}'); + liveSyncChannel.onmessage = function(event) { + if (event.data && event.data.type === 'chat_message' && typeof pollCandidateChat === 'function') { + pollCandidateChat(); + } + }; let codeSyncTimeout = null; editor.onDidChangeModelContent(function () { const code = editor.getValue(); @@ -1824,6 +1829,10 @@ function sendCandidateMessage() { if (!msg) return; input.value = ''; + if (typeof liveSyncChannel !== 'undefined' && liveSyncChannel) { + liveSyncChannel.postMessage({ type: 'chat_message', message: msg, sender: 'candidate' }); + } + fetch(`{{ route('interview.candidate.send-message', $interview->id) }}`, { method: 'POST', headers: { @@ -1834,9 +1843,7 @@ function sendCandidateMessage() { }) .then(r => r.json()) .then(res => { - if (res.success) { - pollCandidateChat(); - } + pollCandidateChat(); }); } diff --git a/resources/views/interview/index.blade.php b/resources/views/interview/index.blade.php index c8830fe..9bf09db 100644 --- a/resources/views/interview/index.blade.php +++ b/resources/views/interview/index.blade.php @@ -749,7 +749,7 @@
No chat history. Send a message below.
- + @@ -1270,6 +1270,8 @@ function subscribeLiveSync(interviewId) { if (drawingImg) drawingImg.style.display = 'none'; if (noDrawing) noDrawing.style.display = 'block'; } + } else if (data.type === 'chat_message') { + pollReviewData(); } }; } @@ -2245,6 +2247,10 @@ function sendSilentWarning() { if (!msg || !currentInterviewId) return; input.value = ''; + if (typeof liveSyncChannel !== 'undefined' && liveSyncChannel) { + liveSyncChannel.postMessage({ type: 'chat_message', message: msg, sender: 'interviewer' }); + } + fetch(`{{ route('interview.warning', ':id') }}`.replace(':id', currentInterviewId), { method: 'POST', headers: { @@ -2255,9 +2261,7 @@ function sendSilentWarning() { }) .then(r => r.json()) .then(res => { - if (res.success) { - pollReviewData(); - } + pollReviewData(); }); }