implement interview pannel
This commit is contained in:
parent
3a736059c5
commit
0d4b9dcfe9
@ -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,27 +420,111 @@ 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');
|
||||||
|
|||||||
@ -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;">×</button>
|
<button onclick="closeReviewModal()" style="background:none; border:none; color:var(--text-muted); font-size:1.5rem; cursor:pointer;">×</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 = '';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user