This commit is contained in:
subhajit 2026-07-31 15:24:24 +05:30
parent c67ee1fceb
commit 367771772f
3 changed files with 189 additions and 157 deletions

View File

@ -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 @@
</div>
@endif
<!-- Global Admin Settings & Proctoring Control Panel -->
<section class="admin-card" style="margin-bottom: 35px; background: linear-gradient(135deg, rgba(15, 23, 42, 0.95) 0%, rgba(30, 41, 59, 0.95) 100%); border: 1.5px solid rgba(99, 102, 241, 0.35); box-shadow: 0 12px 36px rgba(0, 0, 0, 0.5); border-radius: 16px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; border-bottom: 1px solid rgba(255, 255, 255, 0.1); padding-bottom: 12px;">
<div>
<h3 style="font-family: 'Outfit', sans-serif; font-size: 1.3rem; font-weight: 800; color: #ffffff; margin: 0; display: flex; align-items: center; gap: 8px;">
⚙️ Global Admin Proctoring & System Settings
</h3>
<div style="font-size: 0.75rem; color: var(--text-muted); margin-top: 4px;">Master control panel for candidate assessment rules, system screen capturing, and module toggles</div>
</div>
<span class="badge" style="background: rgba(99, 102, 241, 0.2); color: #818cf8; border: 1px solid rgba(99, 102, 241, 0.4); font-weight: 700; padding: 6px 12px; font-size: 0.75rem;">
🛡️ ADMIN CONTROL PANEL
</span>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 16px; margin-top: 16px;">
<!-- 1. Candidate System Screenshot Capture Master Toggle -->
<div style="padding: 16px; background: rgba(16, 185, 129, 0.08); border: 1.5px solid rgba(16, 185, 129, 0.3); border-radius: 12px;">
<form action="{{ route('admin.settings.screenshot-toggle') }}" method="POST">
@csrf
<div style="margin-bottom: 12px;">
<div style="font-size: 0.95rem; color: #ffffff; font-weight: 700; display: flex; align-items: center; gap: 6px;">
📸 Candidate System Screenshot Capture
</div>
<div style="font-size: 0.73rem; color: var(--text-muted); margin-top: 4px; line-height: 1.4;">
Captures candidate system desktop screen automatically on call start & tab switches. Admin can stop taking screenshots anytime across all candidates.
</div>
</div>
<div style="display: flex; justify-content: space-between; align-items: center; padding-top: 10px; border-top: 1px solid rgba(255, 255, 255, 0.08);">
<span style="font-size: 0.75rem; font-weight: 700; color: {{ \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1' ? '#34d399' : '#fca5a5' }};">
{{ \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1' ? '🟢 ENABLED (Active)' : '🔴 STOPPED / DISABLED' }}
</span>
<label style="display: inline-flex; align-items: center; gap: 6px; cursor: pointer;">
<input type="checkbox" name="enable_candidate_screenshots" value="1" onchange="this.form.submit()" {{ \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1' ? 'checked' : '' }} style="accent-color: #10b981; width: 20px; height: 20px; cursor: pointer;">
<span style="font-size: 0.8rem; font-weight: 800; color: white; background: rgba(255,255,255,0.12); padding: 5px 12px; border-radius: 6px;">
{{ \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1' ? 'Stop Screenshot Capture' : 'Enable Screenshot Capture' }}
</span>
</label>
</div>
</form>
</div>
<!-- 2. Onboarding & Offboarding Module Master Toggle -->
<div style="padding: 16px; background: rgba(99, 102, 241, 0.08); border: 1.5px solid rgba(99, 102, 241, 0.3); border-radius: 12px;">
<form action="{{ route('admin.settings.onboarding-toggle') }}" method="POST">
@csrf
<div style="margin-bottom: 12px;">
<div style="font-size: 0.95rem; color: #ffffff; font-weight: 700; display: flex; align-items: center; gap: 6px;">
🚀 Candidate Provisioning & Onboarding Module
</div>
<div style="font-size: 0.73rem; color: var(--text-muted); margin-top: 4px; line-height: 1.4;">
Master toggle to enable/disable automated candidate provisioning, onboarding checklists, and 1-click revocation.
</div>
</div>
<div style="display: flex; justify-content: space-between; align-items: center; padding-top: 10px; border-top: 1px solid rgba(255, 255, 255, 0.08);">
<span style="font-size: 0.75rem; font-weight: 700; color: {{ \App\Models\Setting::get('onboarding_enabled', '1') === '1' ? '#34d399' : '#fca5a5' }};">
{{ \App\Models\Setting::get('onboarding_enabled', '1') === '1' ? '🟢 ENABLED' : '🔴 DISABLED' }}
</span>
<label style="display: inline-flex; align-items: center; gap: 6px; cursor: pointer;">
<input type="checkbox" name="onboarding_enabled" value="1" onchange="this.form.submit()" {{ \App\Models\Setting::get('onboarding_enabled', '1') === '1' ? 'checked' : '' }} style="accent-color: #10b981; width: 20px; height: 20px; cursor: pointer;">
<span style="font-size: 0.8rem; font-weight: 800; color: white; background: rgba(255,255,255,0.12); padding: 5px 12px; border-radius: 6px;">
{{ \App\Models\Setting::get('onboarding_enabled', '1') === '1' ? 'Disable Onboarding' : 'Enable Onboarding' }}
</span>
</label>
</div>
</form>
</div>
</div>
</section>
<!-- Users Table Card -->
<section class="users-card" style="margin-bottom: 50px;">
@ -1241,41 +1175,7 @@
</select>
<button type="submit" class="btn" style="background-color: var(--accent-primary); color: white; font-size:0.75rem; border-radius:8px; padding:8px 12px; border:none; cursor:pointer; font-weight:600; white-space: nowrap;">Save Default</button>
</div>
</div>
</form>
<!-- Onboarding & Offboarding Module Master Toggle -->
<form action="{{ route('admin.settings.onboarding-toggle') }}" method="POST" style="margin-bottom: 12px; padding: 12px 14px; background: rgba(99, 102, 241, 0.08); border: 1px solid rgba(99, 102, 241, 0.25); border-radius: 12px;">
@csrf
<div style="display: flex; justify-content: space-between; align-items: center;">
<div>
<div style="font-size: 0.8rem; color: #ffffff; font-weight: 600;">🚀 Onboarding & Offboarding Module</div>
<div style="font-size: 0.7rem; color: var(--text-muted);">Enable/disable candidate provisioning & 1-click revocation</div>
</div>
<label style="display: inline-flex; align-items: center; gap: 6px; cursor: pointer;">
<input type="checkbox" name="onboarding_enabled" value="1" onchange="this.form.submit()" {{ \App\Models\Setting::get('onboarding_enabled', '1') === '1' ? 'checked' : '' }} style="accent-color: #10b981; width: 18px; height: 18px;">
<span style="font-size: 0.75rem; font-weight: 700; color: {{ \App\Models\Setting::get('onboarding_enabled', '1') === '1' ? '#34d399' : '#fca5a5' }};">
{{ \App\Models\Setting::get('onboarding_enabled', '1') === '1' ? 'ENABLED' : 'DISABLED' }}
</span>
</label>
</div>
</form>
<!-- Candidate System Screenshot Master Toggle (Admin Control) -->
<form action="{{ route('admin.settings.screenshot-toggle') }}" method="POST" style="margin-bottom: 20px; padding: 12px 14px; background: rgba(16, 185, 129, 0.08); border: 1px solid rgba(16, 185, 129, 0.25); border-radius: 12px;">
@csrf
<div style="display: flex; justify-content: space-between; align-items: center;">
<div>
<div style="font-size: 0.8rem; color: #ffffff; font-weight: 600;">📸 Candidate System Screenshot Capture</div>
<div style="font-size: 0.7rem; color: var(--text-muted);">Master Admin control to stop/start candidate system screen capturing (call start & tab switch)</div>
</div>
<label style="display: inline-flex; align-items: center; gap: 6px; cursor: pointer;">
<input type="checkbox" name="enable_candidate_screenshots" value="1" onchange="this.form.submit()" {{ \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1' ? 'checked' : '' }} style="accent-color: #10b981; width: 18px; height: 18px;">
<span style="font-size: 0.75rem; font-weight: 700; color: {{ \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1' ? '#34d399' : '#fca5a5' }};">
{{ \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1' ? 'ENABLED' : 'STOPPED / DISABLED' }}
</span>
</label>
</div>
</div>
</form>
<!-- Registered Roles List -->

View File

@ -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);
</script>
</body>
</html>

View File

@ -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') }}
</div>
@endif
<!-- Admin Proctoring & System Screenshot Control Banner -->
<div style="margin-bottom: 20px; padding: 14px 20px; background: linear-gradient(135deg, rgba(15, 23, 42, 0.9) 0%, rgba(30, 41, 59, 0.9) 100%); border: 1.5px solid rgba(16, 185, 129, 0.35); border-radius: 14px; display: flex; justify-content: space-between; align-items: center; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);">
<div style="display: flex; align-items: center; gap: 12px;">
<div style="font-size: 1.5rem;">📸</div>
<!-- Global Admin Settings & Proctoring Control Panel -->
<section class="admin-card" style="margin-bottom: 25px; padding: 20px; background: linear-gradient(135deg, rgba(15, 23, 42, 0.95) 0%, rgba(30, 41, 59, 0.95) 100%); border: 1.5px solid rgba(99, 102, 241, 0.35); box-shadow: 0 12px 36px rgba(0, 0, 0, 0.5); border-radius: 16px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; border-bottom: 1px solid rgba(255, 255, 255, 0.1); padding-bottom: 12px;">
<div>
<div style="font-size: 0.9rem; font-weight: 700; color: white;">Global Candidate System Screenshot Control</div>
<div style="font-size: 0.75rem; color: var(--text-muted);">
Captures candidate desktop screen on call start & tab switches.
Admin can stop/start screen capturing globally anytime.
</div>
<h3 style="font-family: 'Outfit', sans-serif; font-size: 1.25rem; font-weight: 800; color: #ffffff; margin: 0; display: flex; align-items: center; gap: 8px;">
⚙️ Global Admin Proctoring & System Settings
</h3>
<div style="font-size: 0.75rem; color: var(--text-muted); margin-top: 4px;">Master control panel for candidate assessment rules, system screen capturing, and module toggles</div>
</div>
</div>
<div style="display: flex; align-items: center; gap: 12px;">
<span id="global-screenshot-status-text" style="font-size: 0.8rem; font-weight: 700; color: {{ \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1' ? '#34d399' : '#fca5a5' }};">
{{ \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1' ? '🟢 ENABLED (Screen Capturing Active)' : '🔴 STOPPED / DISABLED' }}
<span class="badge" style="background: rgba(99, 102, 241, 0.2); color: #818cf8; border: 1px solid rgba(99, 102, 241, 0.4); font-weight: 700; padding: 6px 12px; font-size: 0.75rem;">
🛡️ ADMIN CONTROL PANEL
</span>
@if(Auth::user() && Auth::user()->isAdmin())
<form action="{{ route('admin.settings.screenshot-toggle') }}" method="POST" style="margin: 0;">
@csrf
<button type="submit" class="btn-nav" style="padding: 6px 14px; font-size: 0.75rem; font-weight: 700; background: {{ \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1' ? 'rgba(239, 68, 68, 0.2)' : 'rgba(16, 185, 129, 0.2)' }}; color: {{ \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1' ? '#fca5a5' : '#34d399' }}; border: 1px solid {{ \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1' ? '#ef4444' : '#10b981' }}; border-radius: 8px; cursor: pointer;">
{{ \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1' ? '🛑 Stop Screenshot Capture' : '🟢 Start Screenshot Capture' }}
</button>
</form>
@endif
</div>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 16px; margin-top: 16px;">
<!-- 1. Candidate System Screenshot Capture Master Toggle -->
<div style="padding: 16px; background: rgba(16, 185, 129, 0.08); border: 1.5px solid rgba(16, 185, 129, 0.3); border-radius: 12px;">
<form action="{{ route('admin.settings.screenshot-toggle') }}" method="POST">
@csrf
<div style="margin-bottom: 12px;">
<div style="font-size: 0.95rem; color: #ffffff; font-weight: 700; display: flex; align-items: center; gap: 6px;">
📸 Candidate System Screenshot Capture
</div>
<div style="font-size: 0.73rem; color: var(--text-muted); margin-top: 4px; line-height: 1.4;">
Captures candidate system desktop screen automatically on call start & tab switches. Admin can stop taking screenshots anytime across all candidates.
</div>
</div>
<div style="display: flex; justify-content: space-between; align-items: center; padding-top: 10px; border-top: 1px solid rgba(255, 255, 255, 0.08);">
<span id="global-screenshot-status-text" style="font-size: 0.75rem; font-weight: 700; color: {{ \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1' ? '#34d399' : '#fca5a5' }};">
{{ \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1' ? '🟢 ENABLED (Active)' : '🔴 STOPPED / DISABLED' }}
</span>
@if(Auth::user() && Auth::user()->isAdmin())
<label style="display: inline-flex; align-items: center; gap: 6px; cursor: pointer;">
<input type="checkbox" name="enable_candidate_screenshots" value="1" onchange="this.form.submit()" {{ \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1' ? 'checked' : '' }} style="accent-color: #10b981; width: 20px; height: 20px; cursor: pointer;">
<span style="font-size: 0.8rem; font-weight: 800; color: white; background: rgba(255,255,255,0.12); padding: 5px 12px; border-radius: 6px;">
{{ \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1' ? 'Stop Screenshot Capture' : 'Enable Screenshot Capture' }}
</span>
</label>
@endif
</div>
</form>
</div>
<!-- 2. Onboarding & Offboarding Module Master Toggle -->
<div style="padding: 16px; background: rgba(99, 102, 241, 0.08); border: 1.5px solid rgba(99, 102, 241, 0.3); border-radius: 12px;">
<form action="{{ route('admin.settings.onboarding-toggle') }}" method="POST">
@csrf
<div style="margin-bottom: 12px;">
<div style="font-size: 0.95rem; color: #ffffff; font-weight: 700; display: flex; align-items: center; gap: 6px;">
🚀 Candidate Provisioning & Onboarding Module
</div>
<div style="font-size: 0.73rem; color: var(--text-muted); margin-top: 4px; line-height: 1.4;">
Master toggle to enable/disable automated candidate provisioning, onboarding checklists, and 1-click revocation.
</div>
</div>
<div style="display: flex; justify-content: space-between; align-items: center; padding-top: 10px; border-top: 1px solid rgba(255, 255, 255, 0.08);">
<span style="font-size: 0.75rem; font-weight: 700; color: {{ \App\Models\Setting::get('onboarding_enabled', '1') === '1' ? '#34d399' : '#fca5a5' }};">
{{ \App\Models\Setting::get('onboarding_enabled', '1') === '1' ? '🟢 ENABLED' : '🔴 DISABLED' }}
</span>
@if(Auth::user() && Auth::user()->isAdmin())
<label style="display: inline-flex; align-items: center; gap: 6px; cursor: pointer;">
<input type="checkbox" name="onboarding_enabled" value="1" onchange="this.form.submit()" {{ \App\Models\Setting::get('onboarding_enabled', '1') === '1' ? 'checked' : '' }} style="accent-color: #10b981; width: 20px; height: 20px; cursor: pointer;">
<span style="font-size: 0.8rem; font-weight: 800; color: white; background: rgba(255,255,255,0.12); padding: 5px 12px; border-radius: 6px;">
{{ \App\Models\Setting::get('onboarding_enabled', '1') === '1' ? 'Disable Onboarding' : 'Enable Onboarding' }}
</span>
</label>
@endif
</div>
</form>
</div>
</div>
</section>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px;">
<div>
@ -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');