implement interview pannel

This commit is contained in:
subhajit 2026-07-21 13:58:13 +05:30
parent 3a736059c5
commit 0d4b9dcfe9
2 changed files with 340 additions and 38 deletions

View File

@ -10,8 +10,9 @@
<link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500;600&family=Instrument+Sans:wght@400;500;600;700&family=Outfit:wght@500;600;700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500;600&family=Instrument+Sans:wght@400;500;600;700&family=Outfit:wght@500;600;700&display=swap" rel="stylesheet">
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script> <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<!-- Monaco Code Editor Loader --> <!-- Monaco Code Editor Loader & PeerJS Realtime WebRTC Call -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.45.0/min/vs/loader.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.45.0/min/vs/loader.min.js"></script>
<script src="https://unpkg.com/peerjs@1.5.2/dist/peerjs.min.js"></script>
<style> <style>
:root { :root {
@ -238,16 +239,32 @@
</div> </div>
</div> </div>
<!-- Sidebar: Proctored Video Feed & Candidate Info --> <!-- Sidebar: Real-Time 2-Way Interview Call & Screen Share -->
<div class="proctor-sidebar"> <div class="proctor-sidebar">
<div class="webcam-box"> <!-- Candidate Camera Feed -->
<div class="webcam-box" style="position: relative;">
<video id="webcam" autoplay muted playsinline></video> <video id="webcam" autoplay muted playsinline></video>
<div style="position: absolute; bottom: 8px; left: 8px; background: rgba(0,0,0,0.6); padding: 2px 8px; border-radius: 4px; font-size: 0.65rem; color: #34d399; font-weight: 600;"> <div style="position: absolute; bottom: 8px; left: 8px; background: rgba(0,0,0,0.6); padding: 2px 8px; border-radius: 4px; font-size: 0.65rem; color: #34d399; font-weight: 600;">
PROCTORED REC 📷 Candidate Camera
</div> </div>
</div> </div>
<!-- Remote Interviewer Video Feed (Live 2-Way Call) -->
<div class="webcam-box" style="position: relative; border-color: #6366f1;">
<video id="interviewer-video" autoplay playsinline style="width: 100%; height: 100%; object-fit: cover; background: #050811;"></video>
<div id="interviewer-video-placeholder" style="position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; background: rgba(15,23,42,0.9); color: #94a3b8; font-size: 0.75rem; text-align: center; padding: 10px;">
<div style="font-size: 1.5rem; margin-bottom: 4px;">👤</div>
<div>Interviewer Video Feed</div>
<div id="call-status-sub" style="font-size: 0.65rem; color: #38bdf8; margin-top: 4px;">Waiting for panelist to join call...</div>
</div>
<div style="position: absolute; bottom: 8px; left: 8px; background: rgba(99,102,241,0.8); padding: 2px 8px; border-radius: 4px; font-size: 0.65rem; color: white; font-weight: 600;">
🎙️ Interviewer / Panelist
</div>
</div>
<canvas id="proctor-canvas" style="display: none;" width="320" height="240"></canvas> <canvas id="proctor-canvas" style="display: none;" width="320" height="240"></canvas>
<!-- Candidate Profile Info -->
<div style="background: rgba(255,255,255,0.03); border: 1px solid var(--card-border); border-radius: 12px; padding: 14px; font-size: 0.8rem;"> <div style="background: rgba(255,255,255,0.03); border: 1px solid var(--card-border); border-radius: 12px; padding: 14px; font-size: 0.8rem;">
<div style="font-weight: 700; color: white; margin-bottom: 6px;">Candidate Profile</div> <div style="font-weight: 700; color: white; margin-bottom: 6px;">Candidate Profile</div>
<div style="color: #94a3b8;">Name: <strong style="color: white;">{{ $interview->candidate_name }}</strong></div> <div style="color: #94a3b8;">Name: <strong style="color: white;">{{ $interview->candidate_name }}</strong></div>
@ -255,13 +272,25 @@
<div style="color: #94a3b8; margin-top: 6px;">Access Expires: <span style="color: #fca5a5;">{{ $interview->expires_at->format('H:i:s T') }}</span></div> <div style="color: #94a3b8; margin-top: 6px;">Access Expires: <span style="color: #fca5a5;">{{ $interview->expires_at->format('H:i:s T') }}</span></div>
</div> </div>
<div style="background: rgba(239,68,68,0.08); border: 1px solid rgba(239,68,68,0.25); border-radius: 12px; padding: 14px; font-size: 0.75rem; color: #fca5a5;"> <!-- Real-Time Call Controls (Mic, Camera, Screen Sharing) -->
<strong>🛡️ Proctored Assessment Rules:</strong> <div style="background: rgba(99,102,241,0.1); border: 1px solid rgba(99,102,241,0.3); border-radius: 14px; padding: 14px;">
<ul style="margin-top: 6px; padding-left: 16px; line-height: 1.5;"> <div style="font-size: 0.8rem; font-weight: 700; color: white; margin-bottom: 10px; display: flex; align-items: center; justify-content: space-between;">
<li>Do not switch tabs or minimize the browser window.</li> <span>📞 Real-Time Interview Call</span>
<li>Ensure your face is clearly visible in the camera.</li> <span id="call-status-badge" style="font-size: 0.65rem; background: rgba(16,185,129,0.2); color: #34d399; padding: 2px 8px; border-radius: 999px;">READY</span>
<li>External AI overlays or screen sharing will trigger an alert.</li> </div>
</ul>
<div style="display: flex; gap: 8px; margin-bottom: 10px;">
<button id="toggle-mic-btn" onclick="toggleMic()" class="btn-ide" style="flex: 1; padding: 8px; font-size: 0.75rem; background: rgba(16,185,129,0.2); border-color: #10b981;">
🎤 Mic On
</button>
<button id="toggle-cam-btn" onclick="toggleCam()" class="btn-ide" style="flex: 1; padding: 8px; font-size: 0.75rem; background: rgba(16,185,129,0.2); border-color: #10b981;">
📷 Cam On
</button>
</div>
<button id="share-screen-btn" onclick="startScreenShare()" class="btn-ide btn-ide-run" style="width: 100%; padding: 10px; font-size: 0.8rem; background: linear-gradient(135deg, #6366f1 0%, #0078d4 100%);">
🖥️ Share Screen with Interviewer
</button>
</div> </div>
</div> </div>
</div> </div>
@ -391,28 +420,112 @@ function logViolation(type, details) {
}); });
} }
// 4. WebRTC Camera Setup & Eye Gaze Analysis // 4. WebRTC Real-Time 2-Way Interview Call Setup
navigator.mediaDevices.getUserMedia({ video: true, audio: false }) let localMediaStream = null;
let peerInstance = null;
let screenShareStream = null;
let isMicEnabled = true;
let isCamEnabled = true;
navigator.mediaDevices.getUserMedia({ video: true, audio: true })
.then(stream => { .then(stream => {
const videoEl = document.getElementById('webcam'); localMediaStream = stream;
videoEl.srcObject = stream; document.getElementById('webcam').srcObject = stream;
initPeerCall();
// MediaRecorder silent video recording
try {
mediaRecorder = new MediaRecorder(stream, { mimeType: 'video/webm' });
mediaRecorder.ondataavailable = e => {
if (e.data.size > 0) recordedChunks.push(e.data);
};
mediaRecorder.start(10000); // 10s chunks
} catch (err) {}
// Client-side Eye Gaze & Camera Absence Motion Analysis
setInterval(analyzeCameraFrame, 3000);
}) })
.catch(err => { .catch(err => {
logViolation('gaze_anomaly', 'Camera permission denied or video stream disabled'); navigator.mediaDevices.getUserMedia({ video: true, audio: false })
.then(stream => {
localMediaStream = stream;
document.getElementById('webcam').srcObject = stream;
initPeerCall();
});
}); });
function initPeerCall() {
if (typeof Peer === 'undefined') return;
const peerId = 'cand_' + interviewId;
peerInstance = new Peer(peerId);
peerInstance.on('open', id => {
console.log('Candidate WebRTC Peer Registered:', id);
});
// Answer incoming call from Interviewer
peerInstance.on('call', call => {
call.answer(localMediaStream);
call.on('stream', remoteStream => {
const remoteVideo = document.getElementById('interviewer-video');
const placeholder = document.getElementById('interviewer-video-placeholder');
if (remoteVideo) remoteVideo.srcObject = remoteStream;
if (placeholder) placeholder.style.display = 'none';
document.getElementById('call-status-badge').innerText = 'CONNECTED';
document.getElementById('call-status-badge').style.background = 'rgba(16,185,129,0.3)';
});
call.on('close', () => {
const placeholder = document.getElementById('interviewer-video-placeholder');
if (placeholder) placeholder.style.display = 'flex';
document.getElementById('call-status-badge').innerText = 'READY';
});
});
}
function toggleMic() {
if (!localMediaStream) return;
const audioTracks = localMediaStream.getAudioTracks();
if (audioTracks.length > 0) {
isMicEnabled = !isMicEnabled;
audioTracks[0].enabled = isMicEnabled;
const btn = document.getElementById('toggle-mic-btn');
btn.innerText = isMicEnabled ? '🎤 Mic On' : '🔇 Mic Muted';
btn.style.background = isMicEnabled ? 'rgba(16,185,129,0.2)' : 'rgba(239,68,68,0.2)';
btn.style.borderColor = isMicEnabled ? '#10b981' : '#ef4444';
}
}
function toggleCam() {
if (!localMediaStream) return;
const videoTracks = localMediaStream.getVideoTracks();
if (videoTracks.length > 0) {
isCamEnabled = !isCamEnabled;
videoTracks[0].enabled = isCamEnabled;
const btn = document.getElementById('toggle-cam-btn');
btn.innerText = isCamEnabled ? '📷 Cam On' : '🚫 Cam Off';
btn.style.background = isCamEnabled ? 'rgba(16,185,129,0.2)' : 'rgba(239,68,68,0.2)';
btn.style.borderColor = isCamEnabled ? '#10b981' : '#ef4444';
}
}
function startScreenShare() {
if (!navigator.mediaDevices.getDisplayMedia) {
alert('Screen sharing is not supported on your browser.');
return;
}
navigator.mediaDevices.getDisplayMedia({ video: true, audio: false })
.then(stream => {
screenShareStream = stream;
const btn = document.getElementById('share-screen-btn');
btn.innerText = '🟢 Screen Sharing Active';
btn.style.background = '#10b981';
// Connect screen stream to interviewer screen peer
const screenPeer = new Peer('cand_screen_' + interviewId + '_' + Date.now());
screenPeer.on('open', () => {
screenPeer.call('interviewer_screen_' + interviewId, stream);
});
stream.getVideoTracks()[0].onended = () => {
btn.innerText = '🖥️ Share Screen with Interviewer';
btn.style.background = 'linear-gradient(135deg, #6366f1 0%, #0078d4 100%)';
};
})
.catch(err => console.log('Screen sharing cancelled'));
}
function analyzeCameraFrame() { function analyzeCameraFrame() {
const video = document.getElementById('webcam'); const video = document.getElementById('webcam');
const canvas = document.getElementById('proctor-canvas'); const canvas = document.getElementById('proctor-canvas');

View File

@ -9,6 +9,7 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Sans:wght@400;500;600;700&family=Outfit:wght@500;600;700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Instrument+Sans:wght@400;500;600;700&family=Outfit:wght@500;600;700&display=swap" rel="stylesheet">
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script> <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://unpkg.com/peerjs@1.5.2/dist/peerjs.min.js"></script>
<style> <style>
:root { :root {
@ -397,15 +398,79 @@
</div> </div>
</div> </div>
<!-- Modal: Live Review & Code Inspection --> <!-- Modal: Live Review, 2-Way Video Call, Screen Share & Code Inspection -->
<div id="review-modal" class="admin-modal"> <div id="review-modal" class="admin-modal">
<div class="admin-modal-content"> <div class="admin-modal-content" style="max-width: 1100px;">
<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 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>
<h3 id="modal-cand-name" style="font-family: 'Outfit', sans-serif; font-size: 1.25rem; font-weight: 700; color: white; margin: 0;">Review Candidate Session</h3> <h3 id="modal-cand-name" style="font-family: 'Outfit', sans-serif; font-size: 1.25rem; font-weight: 700; color: white; margin: 0;">Review Candidate Session</h3>
<div id="modal-cand-uid" style="font-size: 0.75rem; color: #38bdf8;"></div> <div id="modal-cand-uid" style="font-size: 0.75rem; color: #38bdf8;"></div>
</div> </div>
<button onclick="document.getElementById('review-modal').classList.remove('active')" style="background:none; border:none; color:var(--text-muted); font-size:1.5rem; cursor:pointer;">&times;</button> <button onclick="closeReviewModal()" style="background:none; border:none; color:var(--text-muted); font-size:1.5rem; cursor:pointer;">&times;</button>
</div>
<!-- LIVE 2-WAY VIDEO CALL & SCREEN SHARE PANEL -->
<div style="background: rgba(15,23,42,0.8); border: 1.5px solid #6366f1; border-radius: 16px; padding: 16px; margin-bottom: 20px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
<div style="display: flex; align-items: center; gap: 8px;">
<span style="font-size: 1.1rem;">📞</span>
<strong style="color: white; font-size: 0.95rem;">Real-Time 2-Way Interview Call & Candidate Screen</strong>
<span id="interviewer-call-status" class="badge" style="background: rgba(99,102,241,0.2); color: #818cf8;">DISCONNECTED</span>
</div>
<div style="display: flex; gap: 8px;">
<button id="btn-start-call" onclick="startInterviewerCall()" class="btn-nav" style="background: #10b981; color: white; border: none; font-weight: 700; padding: 6px 14px; cursor: pointer;">
📞 Start 2-Way Call
</button>
<button id="btn-toggle-interviewer-mic" onclick="toggleInterviewerMic()" class="btn-nav" style="display: none; background: rgba(255,255,255,0.1); font-size: 0.75rem;">
🎤 Mic On
</button>
<button id="btn-toggle-interviewer-cam" onclick="toggleInterviewerCam()" class="btn-nav" style="display: none; background: rgba(255,255,255,0.1); font-size: 0.75rem;">
📷 Cam On
</button>
</div>
</div>
<!-- Video Feeds Grid: Candidate Video, Candidate Screen, Interviewer Video -->
<div style="display: grid; grid-template-columns: 1fr 1fr 220px; gap: 12px; min-height: 180px;">
<!-- 1. Candidate Camera Stream -->
<div style="position: relative; background: #050811; border-radius: 12px; border: 1px solid var(--card-border); overflow: hidden; height: 180px;">
<video id="interviewer-cand-video" autoplay playsinline style="width: 100%; height: 100%; object-fit: cover;"></video>
<div id="cand-video-placeholder" style="position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; background: rgba(15,23,42,0.9); color: #94a3b8; font-size: 0.75rem;">
<div style="font-size: 1.8rem;">📷</div>
<div>Candidate Camera Stream</div>
<div style="font-size: 0.65rem; color: #38bdf8; margin-top: 4px;">Click 'Start 2-Way Call' to connect</div>
</div>
<div style="position: absolute; bottom: 6px; left: 6px; background: rgba(0,0,0,0.6); padding: 2px 8px; border-radius: 4px; font-size: 0.65rem; color: #34d399; font-weight: 600;">
👤 Candidate Video
</div>
</div>
<!-- 2. Candidate Live Shared Screen -->
<div style="position: relative; background: #050811; border-radius: 12px; border: 1px solid var(--card-border); overflow: hidden; height: 180px;">
<video id="interviewer-screen-video" autoplay playsinline style="width: 100%; height: 100%; object-fit: contain;"></video>
<div id="screen-video-placeholder" style="position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; background: rgba(15,23,42,0.9); color: #94a3b8; font-size: 0.75rem;">
<div style="font-size: 1.8rem;">🖥️</div>
<div>Candidate Live Screen Share</div>
<div style="font-size: 0.65rem; color: #94a3b8; margin-top: 4px;">Waiting for candidate to share screen...</div>
</div>
<div style="position: absolute; bottom: 6px; left: 6px; background: rgba(0,0,0,0.6); padding: 2px 8px; border-radius: 4px; font-size: 0.65rem; color: #38bdf8; font-weight: 600;">
🖥️ Candidate Shared Screen
</div>
</div>
<!-- 3. Interviewer Self Camera Stream -->
<div style="position: relative; background: #050811; border-radius: 12px; border: 1px solid var(--card-border); overflow: hidden; height: 180px;">
<video id="interviewer-self-video" autoplay muted playsinline style="width: 100%; height: 100%; object-fit: cover;"></video>
<div id="self-video-placeholder" style="position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; background: rgba(15,23,42,0.9); color: #94a3b8; font-size: 0.75rem;">
<div style="font-size: 1.5rem;">🎙️</div>
<div>Interviewer Self Stream</div>
</div>
<div style="position: absolute; bottom: 6px; left: 6px; background: rgba(99,102,241,0.8); padding: 2px 8px; border-radius: 4px; font-size: 0.65rem; color: white; font-weight: 600;">
Panelist Self
</div>
</div>
</div>
</div> </div>
<div style="display: grid; grid-template-columns: 2fr 1fr; gap: 20px; margin-bottom: 20px;"> <div style="display: grid; grid-template-columns: 2fr 1fr; gap: 20px; margin-bottom: 20px;">
@ -423,30 +488,36 @@
<!-- Right: Proctoring Logs & Silent Warning Sender --> <!-- Right: Proctoring Logs & Silent Warning Sender -->
<div> <div>
<span style="font-size: 0.8rem; font-weight: 600; color: white;">Proctoring Violation Audit:</span> <span style="font-size: 0.8rem; font-weight: 600; color: white;">Proctoring Audit:</span>
<div id="modal-logs-display" style="background: rgba(0,0,0,0.3); border: 1px solid var(--card-border); border-radius: 10px; padding: 10px; max-height: 200px; overflow-y: auto; margin-bottom: 14px; font-size: 0.7rem; color: var(--text-muted);"> <div id="modal-logs-display" style="background: rgba(0,0,0,0.3); border: 1px solid var(--card-border); border-radius: 10px; padding: 10px; max-height: 200px; overflow-y: auto; margin-bottom: 14px; font-size: 0.7rem; color: var(--text-muted);">
No proctoring violations recorded. No violations recorded.
</div> </div>
<!-- Silent Warning Form --> <!-- Silent Warning Form -->
<div style="background: rgba(239,68,68,0.08); border: 1px solid rgba(239,68,68,0.25); border-radius: 12px; padding: 12px;"> <div style="background: rgba(239,68,68,0.08); border: 1px solid rgba(239,68,68,0.25); border-radius: 12px; padding: 12px;">
<div style="font-size: 0.75rem; color: #fca5a5; font-weight: 600; margin-bottom: 6px;">Send Silent Warning to Candidate:</div> <div style="font-size: 0.75rem; color: #fca5a5; font-weight: 600; margin-bottom: 6px;">Send Message to Candidate:</div>
<input type="text" id="warning-input" placeholder="e.g. Please keep your camera centered." class="form-input" style="font-size: 0.75rem; margin-bottom: 8px;"> <input type="text" id="warning-input" placeholder="e.g. Please walk me through your solution logic." class="form-input" style="font-size: 0.75rem; margin-bottom: 8px;">
<button onclick="sendSilentWarning()" class="btn-nav" style="width: 100%; background: #ef4444; color: white; border: none; font-size: 0.75rem; font-weight: 600; padding: 6px; cursor: pointer;"> <button onclick="sendSilentWarning()" class="btn-nav" style="width: 100%; background: #ef4444; color: white; border: none; font-size: 0.75rem; font-weight: 600; padding: 6px; cursor: pointer;">
🚨 Send Warning Message 💬 Send Message
</button> </button>
</div> </div>
</div> </div>
</div> </div>
<div style="display: flex; justify-content: flex-end;"> <div style="display: flex; justify-content: flex-end;">
<button type="button" onclick="document.getElementById('review-modal').classList.remove('active')" class="btn-nav">Close</button> <button type="button" onclick="closeReviewModal()" class="btn-nav">Close</button>
</div> </div>
</div> </div>
</div> </div>
<script> <script>
let currentInterviewId = null; let currentInterviewId = null;
let interviewerPeer = null;
let screenReceiverPeer = null;
let interviewerLocalStream = null;
let activeCall = null;
let isInterviewerMicOn = true;
let isInterviewerCamOn = true;
function openReviewModal(id, name, uid) { function openReviewModal(id, name, uid) {
currentInterviewId = id; currentInterviewId = id;
@ -454,6 +525,7 @@ function openReviewModal(id, name, uid) {
document.getElementById('modal-cand-uid').innerText = 'Unique Submission ID: ' + uid; document.getElementById('modal-cand-uid').innerText = 'Unique Submission ID: ' + uid;
document.getElementById('review-modal').classList.add('active'); document.getElementById('review-modal').classList.add('active');
initInterviewerPeer();
// Fetch live poll data // Fetch live poll data
fetch(`{{ route('interview.poll', ':id') }}`.replace(':id', id)) fetch(`{{ route('interview.poll', ':id') }}`.replace(':id', id))
@ -481,6 +553,123 @@ function openReviewModal(id, name, uid) {
}); });
} }
function initInterviewerPeer() {
if (!currentInterviewId || typeof Peer === 'undefined') return;
const peerId = 'interviewer_' + currentInterviewId;
interviewerPeer = new Peer(peerId);
interviewerPeer.on('open', id => {
console.log('Interviewer WebRTC Peer Registered:', id);
});
// Listen for candidate screen share peer stream
const screenPeerId = 'interviewer_screen_' + currentInterviewId;
screenReceiverPeer = new Peer(screenPeerId);
screenReceiverPeer.on('call', call => {
call.answer();
call.on('stream', screenStream => {
const screenVid = document.getElementById('interviewer-screen-video');
const placeholder = document.getElementById('screen-video-placeholder');
if (screenVid) screenVid.srcObject = screenStream;
if (placeholder) placeholder.style.display = 'none';
});
});
}
function startInterviewerCall() {
if (!currentInterviewId) return;
navigator.mediaDevices.getUserMedia({ video: true, audio: true })
.then(stream => {
interviewerLocalStream = stream;
document.getElementById('interviewer-self-video').srcObject = stream;
document.getElementById('self-video-placeholder').style.display = 'none';
document.getElementById('btn-toggle-interviewer-mic').style.display = 'inline-block';
document.getElementById('btn-toggle-interviewer-cam').style.display = 'inline-block';
document.getElementById('btn-start-call').innerText = '🔴 End Call';
document.getElementById('btn-start-call').style.background = '#ef4444';
document.getElementById('btn-start-call').onclick = endInterviewerCall;
const candPeerId = 'cand_' + currentInterviewId;
activeCall = interviewerPeer.call(candPeerId, stream);
if (activeCall) {
activeCall.on('stream', candRemoteStream => {
const candVid = document.getElementById('interviewer-cand-video');
const placeholder = document.getElementById('cand-video-placeholder');
if (candVid) candVid.srcObject = candRemoteStream;
if (placeholder) placeholder.style.display = 'none';
document.getElementById('interviewer-call-status').innerText = 'CONNECTED';
document.getElementById('interviewer-call-status').style.background = 'rgba(16,185,129,0.3)';
document.getElementById('interviewer-call-status').style.color = '#34d399';
});
activeCall.on('close', () => {
endInterviewerCall();
});
}
})
.catch(err => alert('Camera/Mic permission is required to start 2-Way Call.'));
}
function toggleInterviewerMic() {
if (!interviewerLocalStream) return;
const audioTracks = interviewerLocalStream.getAudioTracks();
if (audioTracks.length > 0) {
isInterviewerMicOn = !isInterviewerMicOn;
audioTracks[0].enabled = isInterviewerMicOn;
const btn = document.getElementById('btn-toggle-interviewer-mic');
btn.innerText = isInterviewerMicOn ? '🎤 Mic On' : '🔇 Mic Muted';
btn.style.background = isInterviewerMicOn ? 'rgba(255,255,255,0.1)' : 'rgba(239,68,68,0.3)';
}
}
function toggleInterviewerCam() {
if (!interviewerLocalStream) return;
const videoTracks = interviewerLocalStream.getVideoTracks();
if (videoTracks.length > 0) {
isInterviewerCamOn = !isInterviewerCamOn;
videoTracks[0].enabled = isInterviewerCamOn;
const btn = document.getElementById('btn-toggle-interviewer-cam');
btn.innerText = isInterviewerCamOn ? '📷 Cam On' : '🚫 Cam Off';
btn.style.background = isInterviewerCamOn ? 'rgba(255,255,255,0.1)' : 'rgba(239,68,68,0.3)';
}
}
function endInterviewerCall() {
if (interviewerLocalStream) {
interviewerLocalStream.getTracks().forEach(t => t.stop());
interviewerLocalStream = null;
}
if (activeCall) {
activeCall.close();
activeCall = null;
}
document.getElementById('cand-video-placeholder').style.display = 'flex';
document.getElementById('screen-video-placeholder').style.display = 'flex';
document.getElementById('self-video-placeholder').style.display = 'flex';
document.getElementById('btn-start-call').innerText = '📞 Start 2-Way Call';
document.getElementById('btn-start-call').style.background = '#10b981';
document.getElementById('btn-start-call').onclick = startInterviewerCall;
document.getElementById('btn-toggle-interviewer-mic').style.display = 'none';
document.getElementById('btn-toggle-interviewer-cam').style.display = 'none';
document.getElementById('interviewer-call-status').innerText = 'DISCONNECTED';
document.getElementById('interviewer-call-status').style.background = 'rgba(99,102,241,0.2)';
document.getElementById('interviewer-call-status').style.color = '#818cf8';
}
function closeReviewModal() {
endInterviewerCall();
document.getElementById('review-modal').classList.remove('active');
}
function sendSilentWarning() { function sendSilentWarning() {
const msg = document.getElementById('warning-input').value.trim(); const msg = document.getElementById('warning-input').value.trim();
if (!msg || !currentInterviewId) return; if (!msg || !currentInterviewId) return;
@ -496,7 +685,7 @@ function sendSilentWarning() {
.then(r => r.json()) .then(r => r.json())
.then(res => { .then(res => {
if (res.success) { if (res.success) {
alert('Silent warning sent to candidate screen!'); alert('Message sent to candidate!');
document.getElementById('warning-input').value = ''; document.getElementById('warning-input').value = '';
} }
}); });