sls/resources/views/interview/show.blade.php
kushal.saha 03a36c00e6 feat(UI): add speech indicator
- add a audio monitor
- when any peer speaks, and green outline is shown around the camera card
2026-08-18 07:21:39 +00:00

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&hellip;</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&hellip;
</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&hellip;" 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>