- add a audio monitor - when any peer speaks, and green outline is shown around the camera card
231 lines
13 KiB
PHP
231 lines
13 KiB
PHP
<x-layouts.interview
|
|
:title="$interview->candidate_name . ' - Assessment Review'"
|
|
data-interview-id="{{ $interview->id }}"
|
|
data-submission-id="{{ $interview->submission_unique_id }}"
|
|
>
|
|
@push('head-scripts')
|
|
<script src="https://unpkg.com/peerjs@1.5.2/dist/peerjs.min.js"></script>
|
|
<script src="//cdn.metered.ca/sdk/video/1.4.6/sdk.min.js"></script>
|
|
@endpush
|
|
|
|
<!-- Video & Screen Call Panel -->
|
|
<x-card>
|
|
<x-slot:header>
|
|
<x-interview.candidate-header :interview="$interview" />
|
|
</x-slot:header>
|
|
<x-slot:headerExtra>
|
|
<x-interview.call-controls :interview="$interview" />
|
|
</x-slot:headerExtra>
|
|
|
|
<div class="p-4 lg:p-5 flex flex-col gap-4">
|
|
<div class="grid grid-cols-1 lg:grid-cols-12 gap-4 items-stretch">
|
|
<!-- Stream Viewport -->
|
|
<div class="lg:col-span-8 flex flex-col gap-4 justify-center">
|
|
<div id="primary-stream-container" class="aspect-video bg-[#090d16] border border-[#1b2233] rounded-xl overflow-hidden flex flex-col justify-center">
|
|
<!-- Candidate Video Main Slot (Default active when not sharing screen) -->
|
|
<div id="cand-main-slot" class="w-full h-full">
|
|
<x-video-tile
|
|
id="cand-video-wrapper"
|
|
videoId="interviewer-cand-video"
|
|
placeholderId="cand-video-placeholder"
|
|
badgeText="Candidate video"
|
|
micStatusId="cand-mic-status"
|
|
camStatusId="cand-cam-status"
|
|
showZoom="true"
|
|
zoomInFn="zoomCandVideo(0.25)"
|
|
zoomOutFn="zoomCandVideo(-0.25)"
|
|
zoomResetFn="zoomCandVideo(0)"
|
|
avatarCircleId="modal-cand-avatar-circle"
|
|
avatarNameId="cand-avatar-name"
|
|
avatarTitle="{{ $interview->candidate_name }}"
|
|
initials="{{ $interview->candidate_initials }}">
|
|
<span id="cand-status-text">Waiting for candidate…</span>
|
|
</x-video-tile>
|
|
</div>
|
|
|
|
<!-- Screen Share Tile (Active when candidate shares screen) -->
|
|
<div id="screen-slot" class="w-full h-full hidden">
|
|
<x-video-tile
|
|
id="screen-wrapper"
|
|
videoId="interviewer-screen-video"
|
|
placeholderId="screen-video-placeholder"
|
|
badgeText="Shared screen"
|
|
focused="true"
|
|
showZoom="true"
|
|
showFullscreen="true"
|
|
zoomInFn="zoomScreen(0.25)"
|
|
zoomOutFn="zoomScreen(-0.25)"
|
|
zoomResetFn="resetZoomScreen()"
|
|
fullscreenFn="toggleFullscreenScreen()"
|
|
containerId="screen-zoom-container"
|
|
avatarTitle="Candidate live screen"
|
|
initials="CD">
|
|
Waiting for candidate to share screen…
|
|
</x-video-tile>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Inspector Panel & Dynamic Code/Chat -->
|
|
<div class="lg:col-span-4 flex flex-col gap-3.5">
|
|
<!-- Top Box: Inspector Panel with Proctoring Logs as First & Default Tab -->
|
|
<x-interview.inspector-panel :interview="$interview" />
|
|
|
|
<div class="bg-[#0e1422] border border-[#1b2233] rounded-xl p-3 flex flex-col flex-1">
|
|
<div class="flex items-center justify-between mb-2">
|
|
<div class="inline-flex gap-1 p-0.5 bg-[#090d16] border border-[#1b2233] rounded-lg">
|
|
<button type="button" id="toggle-btn-code" onclick="toggleBottomSection('code')" class="px-2.5 py-1 text-[11px] font-bold rounded-md transition-all bg-indigo-600 text-white cursor-pointer">
|
|
<i class="fa-solid fa-code mr-1"></i> Live Code
|
|
</button>
|
|
<button type="button" id="toggle-btn-chat" onclick="toggleBottomSection('chat')" class="px-2.5 py-1 text-[11px] font-bold rounded-md transition-all text-slate-400 hover:text-white cursor-pointer">
|
|
<i class="fa-solid fa-comments mr-1"></i> Live Chat
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Candidate Live Code Pane -->
|
|
<div id="section-candidate-code" class="flex-1 flex flex-col">
|
|
<pre id="modal-code-display" class="bg-slate-950/80 border border-slate-800 rounded-lg p-2.5 font-mono text-xs text-slate-300 flex-1 min-h-33.75 overflow-y-auto whitespace-pre-wrap leading-relaxed"></pre>
|
|
<div class="text-[10.5px] text-slate-500 mt-2 mb-1 font-semibold uppercase">Execution Output</div>
|
|
<pre id="modal-output-display" class="bg-slate-950/80 border border-slate-800 rounded-lg p-2 font-mono text-[11px] text-emerald-300 max-h-14 overflow-y-auto whitespace-pre-wrap"></pre>
|
|
</div>
|
|
|
|
<!-- Live Chat Pane -->
|
|
<div id="section-live-chat" class="hidden flex-1 flex flex-col justify-between">
|
|
<x-chat-panel id="interviewer-chat-history" class="mb-2 overflow-y-auto h-full" :warnings="$interview->warnings ?? []" :candidateName="$interview->candidate_name" />
|
|
<div class="flex gap-2">
|
|
<input type="text" id="warning-input" placeholder="Type message to candidate…" onkeydown="if(event.key==='Enter') sendSilentWarning()" class="flex-1 bg-[#090d16] border border-[#232b3d] rounded-lg px-3 py-2 text-white text-xs outline-none focus:border-indigo-500 placeholder-[#5b637a]">
|
|
<x-button onclick="sendSilentWarning()" variant="primary" size="sm" icon="fa-solid fa-paper-plane">Send</x-button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center gap-4 overflow-x-auto p-2">
|
|
<!-- Candidate Camera PIP Container (Active when screen share is on) -->
|
|
<div id="cand-pip-slot" class="hidden w-55 aspect-video shrink-0"></div>
|
|
|
|
<!-- Panelist Self Tile -->
|
|
@php
|
|
$usrWords = preg_split('/\s+/', trim(Auth::user()->name ?? 'Interviewer'));
|
|
$usrInitials = count($usrWords) >= 2 ? strtoupper(substr($usrWords[0],0,1).substr(end($usrWords),0,1)) : strtoupper(substr(trim(Auth::user()->name ?? 'IV'),0,2));
|
|
@endphp
|
|
<div class="w-55 aspect-video shrink-0">
|
|
<x-video-tile
|
|
id="self-video-wrapper"
|
|
videoId="interviewer-self-video"
|
|
placeholderId="self-video-placeholder"
|
|
badgeText="Panelist self"
|
|
micStatusId="self-mic-status"
|
|
camStatusId="self-cam-status"
|
|
avatarTitle="{{ Auth::user()->name ?? 'Interviewer' }}"
|
|
initials="{{ $usrInitials }}">
|
|
Panelist (self)
|
|
</x-video-tile>
|
|
</div>
|
|
|
|
<!-- Multi-Party Panelist Video Grid (Appends remote panelists horizontally) -->
|
|
<div id="panelist-mesh-grid" class="flex items-center gap-3 shrink-0"></div>
|
|
</div>
|
|
</div>
|
|
</x-card>
|
|
|
|
<!-- SOS Cheating Alert Modal -->
|
|
<x-interview.sos-modal />
|
|
|
|
@push('scripts')
|
|
<script>
|
|
const interviewSessionKey = 'interview_bottom_section_{{ $interview->id }}';
|
|
|
|
function toggleBottomSection(mode, persist = true) {
|
|
if (persist) {
|
|
try {
|
|
sessionStorage.setItem(interviewSessionKey, mode);
|
|
} catch(e) {}
|
|
}
|
|
|
|
const codeSection = document.getElementById('section-candidate-code');
|
|
const chatSection = document.getElementById('section-live-chat');
|
|
const btnCode = document.getElementById('toggle-btn-code');
|
|
const btnChat = document.getElementById('toggle-btn-chat');
|
|
|
|
if (mode === 'code') {
|
|
if (codeSection) codeSection.classList.remove('hidden');
|
|
if (chatSection) chatSection.classList.add('hidden');
|
|
if (btnCode) {
|
|
btnCode.classList.add('bg-indigo-600', 'text-white');
|
|
btnCode.classList.remove('text-slate-400');
|
|
}
|
|
if (btnChat) {
|
|
btnChat.classList.remove('bg-indigo-600', 'text-white');
|
|
btnChat.classList.add('text-slate-400');
|
|
}
|
|
} else {
|
|
if (codeSection) codeSection.classList.add('hidden');
|
|
if (chatSection) chatSection.classList.remove('hidden');
|
|
if (btnChat) {
|
|
btnChat.classList.add('bg-indigo-600', 'text-white');
|
|
btnChat.classList.remove('text-slate-400');
|
|
}
|
|
if (btnCode) {
|
|
btnCode.classList.remove('bg-indigo-600', 'text-white');
|
|
btnCode.classList.add('text-slate-400');
|
|
}
|
|
}
|
|
}
|
|
window.toggleBottomSection = toggleBottomSection;
|
|
|
|
let previousScreenShareLayout = null;
|
|
function updateScreenShareLayout(isSharing) {
|
|
const candVidWrapper = document.getElementById('cand-video-wrapper');
|
|
const candMainSlot = document.getElementById('cand-main-slot');
|
|
const candPipSlot = document.getElementById('cand-pip-slot');
|
|
const screenSlot = document.getElementById('screen-slot');
|
|
|
|
if (isSharing) {
|
|
if (candVidWrapper && candPipSlot && !candPipSlot.contains(candVidWrapper)) {
|
|
candPipSlot.appendChild(candVidWrapper);
|
|
candPipSlot.classList.remove('hidden');
|
|
}
|
|
if (screenSlot) screenSlot.classList.remove('hidden');
|
|
if (candMainSlot) candMainSlot.classList.add('hidden');
|
|
} else {
|
|
if (candVidWrapper && candMainSlot && !candMainSlot.contains(candVidWrapper)) {
|
|
candMainSlot.appendChild(candVidWrapper);
|
|
candMainSlot.classList.remove('hidden');
|
|
}
|
|
if (candPipSlot) candPipSlot.classList.add('hidden');
|
|
if (screenSlot) screenSlot.classList.add('hidden');
|
|
}
|
|
|
|
// If user has chosen a mode explicitly, respect that mode across all polls/updates
|
|
let activeBottomMode = 'code';
|
|
try {
|
|
activeBottomMode = sessionStorage.getItem(interviewSessionKey) || (isSharing ? 'chat' : 'code');
|
|
} catch(e) {
|
|
activeBottomMode = isSharing ? 'chat' : 'code';
|
|
}
|
|
toggleBottomSection(activeBottomMode, false);
|
|
}
|
|
window.updateScreenShareLayout = updateScreenShareLayout;
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
openReviewModal(
|
|
{{ $interview->id }},
|
|
"{{ addslashes($interview->candidate_name) }}",
|
|
"{{ $interview->submission_unique_id }}",
|
|
{{ request('join_call') == '1' ? 'true' : 'false' }}
|
|
);
|
|
|
|
// Restore active bottom section selection
|
|
try {
|
|
const savedMode = sessionStorage.getItem(interviewSessionKey);
|
|
if (savedMode) {
|
|
toggleBottomSection(savedMode, false);
|
|
}
|
|
} catch(e) {}
|
|
});
|
|
</script>
|
|
@endpush
|
|
</x-layouts.interview>
|