refactor(UI): interviewer panel

- overhaul the interviewer panel setup for more compact design
- assesment scheduling is now in a drawer
- replaced emojies with icon in candidate panel
This commit is contained in:
kushal.saha 2026-08-18 06:07:17 +00:00
parent 834813731b
commit 1c84a162ac
24 changed files with 827 additions and 218 deletions

View File

@ -10,6 +10,7 @@
"license": "MIT",
"require": {
"php": "^8.3",
"gehrisandro/tailwind-merge-laravel": "^1.4",
"laravel/framework": "^13.8",
"laravel/socialite": "^5.28",
"laravel/tinker": "^3.0",

143
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "730221cc898aa9b68b8fcb70bee8e84b",
"content-hash": "d8109d7765ee6f6846cd672d742ad3e6",
"packages": [
{
"name": "brick/math",
@ -644,6 +644,147 @@
],
"time": "2025-12-03T09:33:47+00:00"
},
{
"name": "gehrisandro/tailwind-merge-laravel",
"version": "v1.4.0",
"source": {
"type": "git",
"url": "https://github.com/gehrisandro/tailwind-merge-laravel.git",
"reference": "4dfc54d2f1c148b87a7f9803b3bae74c3437e7d9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/gehrisandro/tailwind-merge-laravel/zipball/4dfc54d2f1c148b87a7f9803b3bae74c3437e7d9",
"reference": "4dfc54d2f1c148b87a7f9803b3bae74c3437e7d9",
"shasum": ""
},
"require": {
"gehrisandro/tailwind-merge-php": "^v1.1.0",
"guzzlehttp/guzzle": "^7.5.1",
"laravel/framework": "^12.0|^13.0",
"php": "^8.2.0"
},
"require-dev": {
"laravel/pint": "^1.13.8",
"orchestra/testbench": "^10.0|^11.0",
"pestphp/pest": "^3.7|^4.4",
"pestphp/pest-plugin-arch": "^3.0|^4.0",
"pestphp/pest-plugin-type-coverage": "^3.3|^4.0",
"phpstan/phpstan": "^1.10.55|^2.1",
"rector/rector": "^0.19|^2.0",
"symfony/var-dumper": "^6.4.2|^7.0"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"TailwindMerge\\Laravel\\TailwindMergeServiceProvider"
]
}
},
"autoload": {
"files": [
"src/helpers.php"
],
"psr-4": {
"TailwindMerge\\Laravel\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Sandro Gehri",
"email": "sandrogehri@gmail.com"
}
],
"description": "TailwindMerge for Laravel merges multiple Tailwind CSS classes by automatically resolving conflicts between them",
"keywords": [
"classes",
"laravel",
"merge",
"php",
"tailwindcss"
],
"support": {
"issues": "https://github.com/gehrisandro/tailwind-merge-laravel/issues",
"source": "https://github.com/gehrisandro/tailwind-merge-laravel/tree/v1.4.0"
},
"funding": [
{
"url": "https://github.com/gehrisandro",
"type": "github"
}
],
"time": "2026-03-21T06:54:49+00:00"
},
{
"name": "gehrisandro/tailwind-merge-php",
"version": "v1.2.0",
"source": {
"type": "git",
"url": "https://github.com/gehrisandro/tailwind-merge-php.git",
"reference": "400040c89bc958ed130699ee4db0213f689cd498"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/gehrisandro/tailwind-merge-php/zipball/400040c89bc958ed130699ee4db0213f689cd498",
"reference": "400040c89bc958ed130699ee4db0213f689cd498",
"shasum": ""
},
"require": {
"php": "^8.2.0",
"psr/simple-cache": "^3.0"
},
"require-dev": {
"laravel/pint": "^1.13.8",
"nunomaduro/collision": "^v8.9.1",
"pestphp/pest": "^v4.4.2",
"pestphp/pest-plugin-type-coverage": "^v4.0.3",
"phpstan/phpstan": "^2.1.42",
"rector/rector": "^2.3.9",
"symfony/var-dumper": "^v7.4.6"
},
"type": "library",
"autoload": {
"files": [
"src/TailwindMerge.php"
],
"psr-4": {
"TailwindMerge\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Sandro Gehri",
"email": "sandrogehri@gmail.com"
}
],
"description": "TailwindMerge for PHP merges multiple Tailwind CSS classes by automatically resolving conflicts between them",
"keywords": [
"classes",
"merge",
"php",
"tailwindcss"
],
"support": {
"issues": "https://github.com/gehrisandro/tailwind-merge-php/issues",
"source": "https://github.com/gehrisandro/tailwind-merge-php/tree/v1.2.0"
},
"funding": [
{
"url": "https://github.com/gehrisandro",
"type": "github"
}
],
"time": "2026-03-21T06:39:15+00:00"
},
{
"name": "graham-campbell/result-type",
"version": "v1.1.4",

View File

@ -626,12 +626,12 @@ export function updateScreenShareButton(active) {
const btn = document.getElementById('share-screen-btn');
if (!btn) return;
if (active) {
btn.innerHTML = '🛑 Stop Screen Sharing';
btn.innerHTML = '<i class="fa-solid fa-stop mr-1.5"></i> <span>Stop Screen Sharing</span>';
btn.style.background = '#ef4444';
btn.style.borderColor = '#dc2626';
btn.onclick = stopScreenShare;
} else {
btn.innerHTML = '🖥️ Share Screen with Interviewer';
btn.innerHTML = '<i class="fa-solid fa-desktop mr-1.5"></i> <span>Share Screen with Interviewer</span>';
btn.style.background = 'linear-gradient(135deg, #6366f1 0%, #0078d4 100%)';
btn.style.borderColor = '#6366f1';
btn.onclick = startScreenShare;

View File

@ -437,22 +437,20 @@ export function addOrUpdateCandidateInterviewerTile(peerId, stream, labelName =
if (!tile) {
tile = document.createElement('div');
tile.id = 'cand-iv-tile-' + peerId;
tile.className = 'webcam-box';
tile.style.position = 'relative';
tile.style.borderColor = '#6366f1';
tile.className = 'w-full aspect-[16/10] bg-black rounded-xl border-2 border-indigo-500/40 overflow-hidden relative shrink-0';
tile.innerHTML = `
<video id="cand-iv-video-${peerId}" autoplay playsinline style="width: 100%; height: 100%; object-fit: cover; background: #050811;"></video>
<div id="cand-iv-placeholder-${peerId}" style="position: absolute; inset: 0; display: ${camOnVal ? 'none' : 'flex'}; flex-direction: column; align-items: center; justify-content: center; background: #0f172a; color: white; z-index: 5;">
<div style="width: 58px; height: 58px; border-radius: 50%; background: linear-gradient(135deg, #38bdf8 0%, #6366f1 100%); display: flex; align-items: center; justify-content: center; font-size: 1.3rem; font-weight: 800; color: white; box-shadow: 0 0 15px rgba(56,189,248,0.4);">
<video id="cand-iv-video-${peerId}" autoplay playsinline class="w-full h-full object-cover bg-slate-950 -scale-x-100"></video>
<div id="cand-iv-placeholder-${peerId}" class="absolute inset-0 ${camOnVal ? 'hidden' : 'flex'} flex-col items-center justify-center bg-slate-900/95 text-slate-400 text-xs text-center p-2.5 z-10">
<div class="w-14 h-14 rounded-full bg-gradient-to-br from-sky-400 to-indigo-500 flex items-center justify-center text-xl font-bold font-outfit text-white mb-1.5 shadow-lg shadow-sky-500/30">
${initials}
</div>
<div style="font-size: 0.8rem; font-weight: 700; color: white; margin-top: 6px;">${labelName}</div>
<div style="font-size: 0.65rem; color: #94a3b8; margin-top: 2px;">Camera Turned Off</div>
<div class="text-xs text-slate-200 font-semibold">${labelName}</div>
<div class="text-[10.5px] text-slate-400 mt-0.5 font-medium">Camera Turned Off</div>
</div>
<div style="position: absolute; bottom: 8px; left: 8px; background: rgba(99,102,241,0.85); padding: 2px 8px; border-radius: 4px; font-size: 0.65rem; color: white; font-weight: 600; z-index: 10; display: flex; align-items: center; gap: 6px;">
<span>🎙 ${labelName}</span>
<span style="opacity: 0.4;">|</span>
<div class="absolute bottom-2 left-2 bg-indigo-600/90 px-2 py-0.5 rounded text-[10.5px] text-white font-semibold z-10 backdrop-blur-xs flex items-center gap-1.5">
<span><i class="fa-solid fa-microphone-lines mr-1 text-[9px]"></i>${labelName}</span>
<span class="opacity-40">|</span>
<i id="cand-iv-mic-${peerId}" class="${micOnVal ? 'fa-solid fa-microphone' : 'fa-solid fa-microphone-slash'}" style="color: ${micOnVal ? '#34d399' : '#ef4444'}; font-size: 10px;" title="${micOnVal ? 'Mic On' : 'Mic Muted'}"></i>
<i id="cand-iv-cam-${peerId}" class="${camOnVal ? 'fa-solid fa-video' : 'fa-solid fa-video-slash'}" style="color: ${camOnVal ? '#34d399' : '#ef4444'}; font-size: 10px;" title="${camOnVal ? 'Camera On' : 'Camera Off'}"></i>
</div>
@ -477,7 +475,13 @@ export function addOrUpdateCandidateInterviewerTile(peerId, stream, labelName =
camIcon.title = camOnVal ? 'Camera On' : 'Camera Off';
}
if (placeholderEl) {
placeholderEl.style.display = camOnVal ? 'none' : 'flex';
if (camOnVal) {
placeholderEl.classList.add('hidden');
placeholderEl.classList.remove('flex');
} else {
placeholderEl.classList.remove('hidden');
placeholderEl.classList.add('flex');
}
}
}
@ -492,7 +496,7 @@ export function addOrUpdateCandidateInterviewerTile(peerId, stream, labelName =
const badge = document.getElementById('call-status-badge');
if (badge) {
badge.innerText = '🟢 CALL CONNECTED (LIVE)';
badge.innerHTML = '<i class="fa-solid fa-circle text-[8px] mr-1 text-emerald-400"></i> CALL LIVE';
badge.style.background = 'rgba(16,185,129,0.3)';
badge.style.color = '#34d399';
}
@ -516,11 +520,11 @@ export function removeCandidateInterviewerTile(peerId) {
const badge = document.getElementById('call-status-badge');
if (badge) {
if (latestCallStatus === 'ended') {
badge.innerText = 'CALL ENDED BY HOST';
badge.innerText = 'CALL ENDED';
badge.style.background = 'rgba(239,68,68,0.2)';
badge.style.color = '#fca5a5';
} else {
badge.innerText = 'READY (Waiting for Interviewer)';
badge.innerText = 'READY';
badge.style.background = 'rgba(16,185,129,0.2)';
badge.style.color = '#34d399';
}
@ -550,7 +554,7 @@ export function triggerCandidateRing(offer = null, callStartedAt = null) {
const badge = document.getElementById('call-status-badge');
if (badge) {
badge.innerText = '🔔 INCOMING CALL... RINGING';
badge.innerHTML = '<i class="fa-solid fa-bell text-[9px] mr-1 text-amber-300 animate-bounce"></i> INCOMING CALL... RINGING';
badge.style.background = 'rgba(234,179,8,0.3)';
badge.style.color = '#fde047';
}
@ -618,7 +622,7 @@ export function showCandidateJoinOption() {
}
const badge = document.getElementById('call-status-badge');
if (badge) {
badge.innerText = '📞 CALL IN PROGRESS';
badge.innerText = 'CALL IN PROGRESS';
badge.style.background = 'rgba(59,130,246,0.3)';
badge.style.color = '#93c5fd';
}
@ -626,7 +630,7 @@ export function showCandidateJoinOption() {
if (joinBtn) {
joinBtn.style.display = 'block';
joinBtn.disabled = false;
joinBtn.innerText = '🟢 📞 Call in Progress • Join Call Now';
joinBtn.innerHTML = '<i class="fa-solid fa-phone-volume mr-1.5"></i> <span>Join Call Now</span>';
}
}
@ -679,7 +683,7 @@ export async function acceptCandidateCall() {
const badge = document.getElementById('call-status-badge');
if (badge) {
badge.innerText = '🟢 CALL CONNECTED (LIVE)';
badge.innerText = 'CALL CONNECTED';
badge.style.background = 'rgba(16,185,129,0.3)';
badge.style.color = '#34d399';
}
@ -791,7 +795,7 @@ export function candidateLeaveCall(isEndedByHost = false) {
}
} else {
if (joinBtn) {
joinBtn.innerText = '🟢 📞 Rejoin Call Now';
joinBtn.innerHTML = '<i class="fa-solid fa-phone-volume mr-1.5"></i> <span>Rejoin Call Now</span>';
joinBtn.style.display = 'block';
joinBtn.disabled = false;
}
@ -1088,9 +1092,10 @@ export function toggleMic() {
audioTracks[0].enabled = isCandidateMicEnabled;
const btn = document.getElementById('toggle-mic-btn');
if (btn) {
btn.innerText = isCandidateMicEnabled ? '🎤 Mic On' : '🔇 Mic Muted';
btn.innerHTML = `<i class="${isCandidateMicEnabled ? 'fa-solid fa-microphone' : 'fa-solid fa-microphone-slash'}"></i> <span>${isCandidateMicEnabled ? 'Mic On' : 'Mic Off'}</span>`;
btn.style.background = isCandidateMicEnabled ? 'rgba(16,185,129,0.2)' : 'rgba(239,68,68,0.2)';
btn.style.borderColor = isCandidateMicEnabled ? '#10b981' : '#ef4444';
btn.className = `flex-1 py-2 px-2 text-xs font-semibold rounded-lg ${isCandidateMicEnabled ? 'bg-emerald-500/20 border-emerald-500 text-emerald-300 hover:bg-emerald-500/30' : 'bg-red-500/20 border-red-500 text-red-300 hover:bg-red-500/30'} border transition-all cursor-pointer flex items-center justify-center gap-1.5`;
}
const icon = document.getElementById('cand-self-mic-icon');
if (icon) {
@ -1111,9 +1116,10 @@ export function toggleCam() {
videoTracks[0].enabled = isCandidateCamEnabled;
const btn = document.getElementById('toggle-cam-btn');
if (btn) {
btn.innerText = isCandidateCamEnabled ? '📷 Cam On' : '🚫 Cam Off';
btn.innerHTML = `<i class="${isCandidateCamEnabled ? 'fa-solid fa-video' : 'fa-solid fa-video-slash'}"></i> <span>${isCandidateCamEnabled ? 'Cam On' : 'Cam Off'}</span>`;
btn.style.background = isCandidateCamEnabled ? 'rgba(16,185,129,0.2)' : 'rgba(239,68,68,0.2)';
btn.style.borderColor = isCandidateCamEnabled ? '#10b981' : '#ef4444';
btn.className = `flex-1 py-2 px-2 text-xs font-semibold rounded-lg ${isCandidateCamEnabled ? 'bg-emerald-500/20 border-emerald-500 text-emerald-300 hover:bg-emerald-500/30' : 'bg-red-500/20 border-red-500 text-red-300 hover:bg-red-500/30'} border transition-all cursor-pointer flex items-center justify-center gap-1.5`;
}
if (avatarPlaceholder) avatarPlaceholder.style.display = isCandidateCamEnabled ? 'none' : 'flex';
}

View File

@ -362,8 +362,71 @@ export function updateScreenshotToggleBtnUI(enabled) {
}
}
export function switchIdeTab(tab) {
const tabs = ['code', 'notes', 'drawing', 'recordings', 'screenshots'];
let currentInspectorTab = null;
export function closeInspectorPanel() {
currentInspectorTab = null;
try {
if (currentInterviewId) {
sessionStorage.removeItem('interview_inspector_tab_' + currentInterviewId);
}
} catch(e) {}
const details = document.getElementById('inspector-details');
if (details) details.open = false;
const tabs = ['logs', 'code', 'notes', 'drawing', 'recordings', 'screenshots'];
tabs.forEach(t => {
const contentEl = document.getElementById(`tab-content-${t}`);
const btnEl = document.getElementById(`tab-btn-${t}`);
if (contentEl) contentEl.style.display = 'none';
if (btnEl) {
btnEl.classList.remove('bg-indigo-600', 'text-white');
btnEl.classList.add('text-slate-400', 'hover:bg-white/5');
}
});
}
export function switchIdeTab(tab, event = null) {
if (event) {
event.stopPropagation();
}
const details = document.getElementById('inspector-details');
// If user clicks the currently active open tab button, toggle it collapsed
if (currentInspectorTab === tab && details && details.open) {
closeInspectorPanel();
return;
}
currentInspectorTab = tab;
try {
if (currentInterviewId) {
sessionStorage.setItem('interview_inspector_tab_' + currentInterviewId, tab);
}
} catch(e) {}
if (details) details.open = true;
const tabs = ['logs', 'code', 'notes', 'drawing', 'recordings', 'screenshots'];
const tabMeta = {
logs: { title: 'Proctoring Logs', icon: 'fa-solid fa-shield-halved' },
notes: { title: 'Candidate Notepad', icon: 'fa-solid fa-note-sticky' },
drawing: { title: 'Candidate Drawing Board', icon: 'fa-solid fa-pen' },
recordings: { title: 'Saved Video Recordings', icon: 'fa-solid fa-film' },
screenshots: { title: 'Tab Switch Screenshots', icon: 'fa-solid fa-image' },
code: { title: 'Candidate Code', icon: 'fa-solid fa-code' }
};
const floatingTitle = document.getElementById('inspector-floating-text');
const floatingIcon = document.getElementById('inspector-floating-icon');
if (tabMeta[tab]) {
if (floatingTitle) floatingTitle.innerText = tabMeta[tab].title;
if (floatingIcon) floatingIcon.className = `${tabMeta[tab].icon} text-indigo-400`;
}
tabs.forEach(t => {
const contentEl = document.getElementById(`tab-content-${t}`);
const btnEl = document.getElementById(`tab-btn-${t}`);
@ -372,16 +435,26 @@ export function switchIdeTab(tab) {
}
if (btnEl) {
if (t === tab) {
btnEl.classList.add('bg-indigo-600', 'text-white', 'border-indigo-500');
btnEl.classList.remove('bg-white/5', 'text-slate-400', 'border-slate-700/60');
btnEl.classList.add('bg-indigo-600', 'text-white');
btnEl.classList.remove('text-slate-400', 'hover:bg-white/5');
} else {
btnEl.classList.remove('bg-indigo-600', 'text-white', 'border-indigo-500');
btnEl.classList.add('bg-white/5', 'text-slate-400', 'border-slate-700/60');
btnEl.classList.remove('bg-indigo-600', 'text-white');
btnEl.classList.add('text-slate-400', 'hover:bg-white/5');
}
}
});
}
// Click outside to dismiss floating inspector details
if (typeof document !== 'undefined') {
document.addEventListener('click', function(e) {
const details = document.getElementById('inspector-details');
if (details && details.open && !details.contains(e.target)) {
closeInspectorPanel();
}
});
}
export function triggerIncomingCallRing(callData, isRealtimeBroadcast = false) {
if (!callData || !callData.id) return;
if (typeof document !== 'undefined' && document.body && document.body.dataset.interviewId) return;
@ -609,7 +682,24 @@ export function openReviewModal(id, name, uid, autoJoinCall = false) {
if (reviewModal) reviewModal.classList.add('active');
resetZoomScreen();
if (document.getElementById('inspector-content-container')) {
let savedInspectorTab = null;
try {
savedInspectorTab = sessionStorage.getItem('interview_inspector_tab_' + id);
} catch(e) {}
if (savedInspectorTab) {
switchIdeTab(savedInspectorTab);
} else {
closeInspectorPanel();
}
} else if (document.getElementById('tab-btn-logs')) {
switchIdeTab('logs');
} else if (document.getElementById('tab-btn-notes')) {
switchIdeTab('notes');
} else {
switchIdeTab('code');
}
updateCandidateStatusIcons(false, false, false);
initInterviewerSigChannel();
@ -783,6 +873,9 @@ export function pollReviewData() {
const screenPlace = document.getElementById('screen-video-placeholder');
if (screenVid) screenVid.srcObject = null;
if (screenPlace) screenPlace.style.display = 'flex';
if (typeof window.updateScreenShareLayout === 'function') {
window.updateScreenShareLayout(false);
}
panelistMeshTiles.forEach((_, pid) => {
removePanelistTile(pid);
@ -957,7 +1050,7 @@ export function pollReviewData() {
const badgeLabel = isCand ? 'Candidate' : 'Panelist';
const badgeClass = isCand ? 'bg-sky-500/20 text-sky-300 border-sky-500/30' : 'bg-indigo-500/20 text-indigo-300 border-indigo-500/30';
chatHtml += `<div class="chat-message mb-2 p-2.5 rounded-lg border-l-2 transition-all w-full shrink-0" style="background: ${color}1a; border-color: ${color};">
chatHtml += `<div class="chat-message mb-2 p-2.5 rounded-lg transition-all w-full shrink-0" style="background: ${color}1a; border-color: ${color};">
<div class="flex justify-between items-center mb-1 gap-2">
<div class="flex items-center gap-1.5 min-w-0">
<span class="w-1.5 h-1.5 rounded-full shrink-0" style="background: ${color};"></span>
@ -969,11 +1062,11 @@ export function pollReviewData() {
<div class="text-slate-200 text-xs leading-relaxed font-medium pl-3 break-words">${w.message}</div>
</div>`;
});
chatHistory.className = 'bg-[#0d1220] border border-[#1b2233] rounded-[9px] h-[150px] overflow-y-auto p-3 flex flex-col gap-2 mb-2.5';
chatHistory.className = 'bg-[#0d1220] border border-[#1b2233] rounded-[9px] h-full overflow-y-auto p-3 flex flex-col gap-2 mb-2.5';
chatHistory.innerHTML = chatHtml;
chatHistory.scrollTop = chatHistory.scrollHeight;
} else {
chatHistory.className = 'bg-[#0d1220] border border-[#1b2233] rounded-[9px] h-[150px] overflow-y-auto p-3 mb-2.5';
chatHistory.className = 'bg-[#0d1220] border border-[#1b2233] rounded-[9px] h-full overflow-y-auto p-3 mb-2.5';
chatHistory.innerHTML = '<div class="h-full w-full flex items-center justify-center text-xs text-[#5b637a] italic">No chat history. Send a message below.</div>';
}
}
@ -1419,7 +1512,7 @@ export function addOrUpdatePanelistTile(peerId, stream, name = 'Panelist', micOn
if (!tile) {
tile = document.createElement('div');
tile.id = 'panelist-tile-' + peerId;
tile.className = 'video-tile relative aspect-[16/11] bg-[#0d1220] border border-[#1b2233] rounded-[10px] overflow-hidden flex flex-col items-center justify-center transition-all';
tile.className = 'video-tile relative w-[220px] aspect-video shrink-0 bg-[#0d1220] border border-[#1b2233] rounded-[10px] overflow-hidden flex flex-col items-center justify-center transition-all';
tile.innerHTML = `
<video id="panelist-video-${peerId}" autoplay playsinline class="w-full h-full object-cover origin-center transition-transform duration-200" style="transform: scaleX(-1);"></video>
@ -1513,6 +1606,18 @@ export async function initInterviewerPeer() {
console.log('[WebRTC Screen] Attaching incoming screen stream to video element');
const screenVid = document.getElementById('interviewer-screen-video');
const placeholder = document.getElementById('screen-video-placeholder');
if (typeof window.updateScreenShareLayout === 'function') {
window.updateScreenShareLayout(true);
}
if (screenStream && screenStream.getVideoTracks) {
screenStream.getVideoTracks().forEach(track => {
track.onended = () => {
if (typeof window.updateScreenShareLayout === 'function') {
window.updateScreenShareLayout(false);
}
};
});
}
if (screenVid && screenStream) {
screenVid.muted = true;
if (screenVid.srcObject !== screenStream) {
@ -2355,6 +2460,7 @@ if (typeof window !== 'undefined') {
window.closeReviewModal = closeReviewModal;
window.sendSilentWarning = sendSilentWarning;
window.setCurrentInterviewId = setCurrentInterviewId;
window.closeInspectorPanel = closeInspectorPanel;
// Attach listeners
['click', 'keydown', 'touchstart'].forEach(evt => {

View File

@ -2,7 +2,7 @@
'logs' => [],
])
<div {{ $attributes->merge(['class' => 'bg-[#0d1220] border border-[#1b2233] rounded-[9px] max-h-[260px] overflow-y-auto']) }}>
<div {{ $attributes->twMerge(['class' => 'bg-[#0d1220] border border-[#1b2233] rounded-[9px] overflow-y-auto']) }}>
@if(is_array($logs) && count($logs) > 0)
@foreach($logs as $log)
<x-audit-log-item

View File

@ -31,7 +31,7 @@
]);
@endphp
<button type="{{ $type }}" {{ $attributes->merge(['class' => $classes]) }}>
<button type="{{ $type }}" {{ $attributes->twMerge(['class' => $classes]) }}>
@if($icon)
<i class="{{ $icon }}"></i>
@endif

View File

@ -25,7 +25,7 @@
$borderRgba = "rgba({$r}, {$g}, {$b}, 0.35)";
@endphp
<div {{ $attributes->merge(['class' => 'chat-message mb-2 p-2.5 rounded-lg border-l-2 transition-all']) }} style="background: {{ $bgRgba }}; border-color: {{ $resolvedColor }};">
<div {{ $attributes->merge(['class' => 'chat-message mb-2 p-2.5 rounded-lg transition-all']) }} style="background: {{ $bgRgba }}; border-color: {{ $resolvedColor }};">
<div class="flex justify-between items-center mb-1 gap-2">
<div class="flex items-center gap-1.5 min-w-0">
<span class="w-1.5 h-1.5 rounded-full shrink-0" style="background: {{ $resolvedColor }};"></span>

View File

@ -15,7 +15,7 @@
];
@endphp
<div {{ $attributes->merge(['class' => 'bg-[#0d1220] border border-[#1b2233] rounded-[9px] h-[150px] overflow-y-auto p-3 flex flex-col gap-2']) }}>
<div {{ $attributes->twMerge(['class' => 'bg-[#0d1220] border border-[#1b2233] rounded-[9px] overflow-y-auto p-3 flex flex-col gap-2']) }}>
@if(count($warnings) > 0)
@foreach($warnings as $w)
@php
@ -30,7 +30,7 @@
@endphp
<x-chat-message
:senderName="$senderName"
:isCandidate="$isCand"
:isCandidate="
:color="$color"
:message="$w->message"
:timestamp="$w->created_at"

View File

@ -0,0 +1,61 @@
@props([
'id',
'title' => null,
'description' => null,
'width' => 'lg',
'side' => 'right',
'onClose' => null,
])
@php
$widths = [
'sm' => 'max-w-sm',
'md' => 'max-w-md',
'lg' => 'max-w-lg',
'xl' => 'max-w-xl',
'2xl' => 'max-w-2xl',
'3xl' => 'max-w-3xl',
'full' => 'max-w-full',
];
$widthClass = $widths[$width] ?? $widths['lg'];
$isLeft = $side === 'left';
$justifyClass = $isLeft ? 'justify-start' : 'justify-end';
$translateClosed = $isLeft ? '-translate-x-full' : 'translate-x-full';
$borderClass = $isLeft ? 'border-r' : 'border-l';
@endphp
<div id="{{ $id }}" {{ $attributes->merge(['class' => "admin-drawer fixed inset-0 w-screen h-screen z-[1000] opacity-0 pointer-events-none transition-opacity duration-300 [&.active]:opacity-100 [&.active]:pointer-events-auto flex $justifyClass"]) }}>
<!-- Backdrop Overlay Target -->
<div class="fixed inset-0" onclick="document.getElementById('{{ $id }}').classList.remove('active'); {{ $onClose ? $onClose . '();' : '' }}"></div>
<!-- Slide-Over Drawer Content Container -->
<div class="drawer-panel relative z-10 w-full {{ $widthClass }} h-full bg-slate-900 {{ $borderClass }} border-slate-700/60 shadow-2xl flex flex-col text-white transform {{ $translateClosed }} transition-transform duration-300 ease-out [.active_&]:translate-x-0">
<!-- Header -->
<div class="flex items-start justify-between p-6 border-b border-white/10 shrink-0">
<div class="pr-4">
@if($title)
<h3 class="font-outfit text-xl font-bold text-white m-0 leading-snug">{{ $title }}</h3>
@endif
@if($description)
<p class="text-xs text-slate-400 mt-1 mb-0 leading-relaxed">{{ $description }}</p>
@endif
</div>
<button type="button" onclick="document.getElementById('{{ $id }}').classList.remove('active'); {{ $onClose ? $onClose . '();' : '' }}" class="w-8 h-8 rounded-lg bg-slate-800/80 border border-slate-700/60 text-slate-400 hover:text-white hover:bg-slate-700/60 transition-colors flex items-center justify-center cursor-pointer shrink-0 text-sm focus:outline-none focus:ring-1 focus:ring-indigo-500">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
<!-- Scrollable Body Content -->
<div class="flex-1 overflow-y-auto p-6 space-y-4">
{{ $slot }}
</div>
<!-- Optional Fixed Footer -->
@if(isset($footer))
<div class="p-6 border-t border-white/10 bg-slate-950/40 shrink-0">
{{ $footer }}
</div>
@endif
</div>
</div>

View File

@ -28,7 +28,7 @@
<x-button id="btn-start-recording" onclick="startCallRecording()" variant="secondary" icon="fa-solid fa-circle-dot" class="hidden">
Record Call (Silent)
</x-button>
<x-button id="btn-stop-recording" onclick="stopCallRecording()" variant="danger" icon="fa-solid fa-square" class="hidden animate-pulse" title="Stop &amp; Save Recording"></x-button>
<x-button id="btn-stop-recording" class="hidden" onclick="stopCallRecording()" variant="danger" icon="fa-solid fa-square" class="hidden animate-pulse" title="Stop &amp; Save Recording"></x-button>
<!-- 5. Leave Call -->
<x-button id="btn-leave-call" onclick="leaveInterviewerCall()" variant="secondary" icon="fa-solid fa-arrow-right-from-bracket" class="hidden">

View File

@ -4,7 +4,6 @@
<div class="flex items-center gap-3 min-w-0 flex-wrap sm:flex-nowrap">
<x-hamburger-menu align="left">
<!-- Section 1: Session Details 4-Grid -->
<div class="pb-3">
<div class="text-[11px] font-bold text-slate-400 uppercase tracking-wider mb-2 flex items-center gap-1.5">
<i class="fa-solid fa-circle-info text-indigo-400"></i> Session Details
@ -29,14 +28,13 @@
</div>
</div>
<!-- Section 2: SOS Auto-Trigger & Other Actions -->
<div class="pt-3 flex flex-col gap-2.5">
<div class="flex items-center justify-between px-1 text-xs text-slate-300 cursor-pointer select-none" onclick="toggleSosAutoTrigger()">
<span class="flex items-center gap-2 font-medium">
<i class="fa-solid fa-shield-halved text-indigo-400"></i>
<span>SOS auto-trigger alert</span>
</span>
<span class="toggle relative inline-flex h-4 w-8 shrink-0 cursor-pointer rounded-full border border-slate-700 bg-slate-900 transition-colors duration-200 ease-in-out [&.on]:bg-indigo-500/20 [&.on]:border-indigo-500/50 after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:h-2.5 after:w-2.5 after:rounded-full after:bg-slate-500 after:transition-all duration-150 [&.on]:after:left-[17px] [&.on]:after:bg-indigo-400" id="sosToggle"></span>
<span class="toggle relative inline-flex h-4 w-8 shrink-0 cursor-pointer rounded-full border border-slate-700 bg-slate-900 transition-colors duration-200 ease-in-out [&.on]:bg-indigo-500/20 [&.on]:border-indigo-500/50 after:content-[''] after:absolute after:top-0.5 after:left-0.5 after:h-2.5 after:w-2.5 after:rounded-full after:bg-slate-500 after:transition-all [&.on]:after:left-4.25 [&.on]:after:bg-indigo-400" id="sosToggle"></span>
</div>
<div class="grid grid-cols-3 gap-1.5 pt-1">
@ -48,7 +46,7 @@
</x-hamburger-menu>
<!-- Candidate Avatar Circle -->
<div class="w-8 h-8 rounded-full bg-gradient-to-br from-indigo-500 to-sky-600 flex items-center justify-center text-white font-bold text-xs shrink-0 shadow-md shadow-indigo-500/20">
<div class="w-8 h-8 rounded-full bg-linear-to-br from-indigo-500 to-sky-600 flex items-center justify-center text-white font-bold text-xs shrink-0 shadow-md shadow-indigo-500/20">
{{ $interview->candidate_initials }}
</div>

View File

@ -1,19 +1,25 @@
<div id="drawing-modal" class="fixed top-[70px] right-[360px] w-[550px] h-[420px] bg-slate-900 border-2 border-cyan-500 rounded-2xl shadow-2xl hidden flex-col z-50 overflow-hidden">
<div class="bg-slate-800 px-4 py-2.5 flex justify-between items-center border-b border-white/10">
<div class="font-bold text-xs text-white flex items-center gap-1.5">
<span>🎨</span>
<div class="font-bold text-xs text-white flex items-center gap-2">
<i class="fa-solid fa-pen-ruler text-cyan-400"></i>
<span>Candidate Live Drawing Board</span>
</div>
<div class="flex items-center gap-2">
<input type="color" id="draw-color" value="#38bdf8" class="w-6 h-6 border-none rounded-full cursor-pointer bg-transparent">
<button type="button" onclick="clearCanvasDraw()" class="px-2 py-0.5 text-[11px] font-semibold bg-white/10 hover:bg-white/20 text-white rounded border border-white/10 cursor-pointer transition-colors">Clear</button>
<button type="button" onclick="toggleDrawingModal()" class="bg-transparent border-none text-slate-400 hover:text-white text-lg cursor-pointer leading-none">&times;</button>
<button type="button" onclick="clearCanvasDraw()" class="px-2.5 py-1 text-[11px] font-semibold bg-white/10 hover:bg-white/20 text-white rounded-md border border-white/10 cursor-pointer transition-colors flex items-center gap-1">
<i class="fa-solid fa-eraser text-[10px]"></i>
<span>Clear</span>
</button>
<button type="button" onclick="toggleDrawingModal()" class="w-6 h-6 rounded-md bg-slate-700/60 hover:bg-slate-700 text-slate-300 hover:text-white flex items-center justify-center cursor-pointer text-xs transition-colors">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
</div>
<div class="flex-1 bg-[#050811] relative">
<canvas id="drawing-canvas" width="550" height="340" class="cursor-crosshair block w-full h-full"></canvas>
</div>
<div class="px-3 py-1.5 bg-slate-900 text-[10.5px] text-cyan-400 text-right border-t border-white/5 font-medium">
🎨 Draw architecture / logic diagrams (Auto-synced to Interviewer)
<div class="px-3 py-1.5 bg-slate-900 text-[10.5px] text-cyan-400 text-right border-t border-white/5 font-medium flex items-center justify-end gap-1.5">
<i class="fa-solid fa-diagram-project text-[10px]"></i>
<span>Draw architecture / logic diagrams (Auto-synced to Interviewer)</span>
</div>
</div>

View File

@ -4,24 +4,25 @@
<header class="h-[60px] bg-slate-900 border-b border-white/10 flex items-center justify-between px-5 z-20 shrink-0">
<div class="flex items-center gap-3">
<div class="font-outfit font-bold text-base text-white">
Candidate Assessment Room
<div class="font-outfit font-bold text-base text-white flex items-center gap-2">
<i class="fa-solid fa-laptop-code text-indigo-400"></i>
<span>Candidate Assessment Room</span>
</div>
<code class="bg-white/10 px-2.5 py-1 rounded-md text-sky-400 text-xs font-mono">
ID: {{ $interview->submission_unique_id }}
</code>
</div>
<div class="flex items-center gap-3">
<button type="button" onclick="toggleNotepadModal()" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-semibold bg-indigo-500/20 border border-indigo-500/40 text-indigo-300 hover:bg-indigo-500/30 transition-all cursor-pointer">
📝 Open Notepad
</button>
<div class="flex items-center gap-2.5">
<x-button type="button" onclick="toggleNotepadModal()" variant="secondary" size="sm" icon="fa-solid fa-note-sticky" class="bg-indigo-500/20 text-indigo-300 border-indigo-500/40 hover:bg-indigo-500/30">
Notepad
</x-button>
<button type="button" onclick="toggleDrawingModal()" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-semibold bg-cyan-500/20 border border-cyan-500/40 text-cyan-300 hover:bg-cyan-500/30 transition-all cursor-pointer">
🎨 Open Drawing
</button>
<x-button type="button" onclick="toggleDrawingModal()" variant="secondary" size="sm" icon="fa-solid fa-pen-ruler" class="bg-cyan-500/20 text-cyan-300 border-cyan-500/40 hover:bg-cyan-500/30">
Drawing Board
</x-button>
<div class="flex items-center gap-2">
<div class="flex items-center gap-1.5">
<label class="text-xs text-slate-400 font-medium">Language:</label>
<select id="language-select" class="px-2.5 py-1.5 rounded-lg text-xs font-semibold bg-slate-800 border border-white/10 text-white outline-none focus:border-indigo-500 transition-all cursor-pointer">
<option value="python" {{ strtolower($interview->language) === 'python' ? 'selected' : '' }}>Python 3</option>
@ -33,16 +34,16 @@
</select>
</div>
<button type="button" onclick="runCode()" class="inline-flex items-center gap-1.5 px-3.5 py-1.5 rounded-lg text-xs font-bold bg-indigo-600 border border-indigo-500 text-white hover:bg-indigo-500 shadow-md shadow-indigo-600/30 transition-all cursor-pointer">
Run Code
</button>
<x-button type="button" onclick="runCode()" variant="primary" size="sm" icon="fa-solid fa-play" class="shadow-md shadow-indigo-600/30">
Run Code
</x-button>
<button type="button" onclick="submitSolution()" class="inline-flex items-center gap-1.5 px-3.5 py-1.5 rounded-lg text-xs font-bold bg-emerald-600 border border-emerald-500 text-white hover:bg-emerald-500 shadow-md shadow-emerald-600/30 transition-all cursor-pointer">
Submit Final Solution
</button>
<x-button type="button" onclick="submitSolution()" variant="success" size="sm" icon="fa-solid fa-check" class="shadow-md shadow-emerald-600/30">
Submit Solution
</x-button>
<button type="button" onclick="candidateLogoutAction()" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-bold bg-red-500/20 border border-red-500/40 text-red-300 hover:bg-red-500/30 transition-all cursor-pointer">
🚪 Logout
</button>
<x-button type="button" onclick="candidateLogoutAction()" variant="danger" size="sm" icon="fa-solid fa-right-from-bracket" class="bg-red-500/20 border-red-500/40 text-red-300 hover:bg-red-500/30">
Logout
</x-button>
</div>
</header>

View File

@ -8,8 +8,9 @@
</div>
</div>
<div class="text-[11px] uppercase tracking-widest text-emerald-400 font-extrabold mb-1.5">
INCOMING INTERVIEW CALL
<div class="text-[11px] uppercase tracking-widest text-emerald-400 font-extrabold mb-1.5 flex items-center justify-center gap-1.5">
<i class="fa-solid fa-bolt"></i>
<span>INCOMING INTERVIEW CALL</span>
</div>
<h3 class="font-outfit text-2xl font-extrabold text-white mb-1">
@ -21,11 +22,13 @@
</div>
<div class="flex gap-3 justify-center">
<button type="button" onclick="declineCandidateCall()" class="flex-1 py-3 px-4 bg-red-500/20 border border-red-500 text-red-300 font-bold text-xs rounded-xl hover:bg-red-500/30 transition-all cursor-pointer">
🛑 Decline / Busy
<button type="button" onclick="declineCandidateCall()" class="flex-1 py-3 px-4 bg-red-500/20 border border-red-500 text-red-300 font-bold text-xs rounded-xl hover:bg-red-500/30 transition-all cursor-pointer flex items-center justify-center gap-1.5">
<i class="fa-solid fa-phone-slash"></i>
<span>Decline / Busy</span>
</button>
<button type="button" onclick="acceptCandidateCall()" class="flex-[1.4] py-3 px-4 bg-gradient-to-r from-emerald-500 to-emerald-600 text-white font-extrabold text-xs rounded-xl border-none shadow-lg shadow-emerald-500/50 hover:from-emerald-600 hover:to-emerald-700 transition-all cursor-pointer animate-pulse">
📞 Accept Call
<button type="button" onclick="acceptCandidateCall()" class="flex-[1.4] py-3 px-4 bg-gradient-to-r from-emerald-500 to-emerald-600 text-white font-extrabold text-xs rounded-xl border-none shadow-lg shadow-emerald-500/50 hover:from-emerald-600 hover:to-emerald-700 transition-all cursor-pointer animate-pulse flex items-center justify-center gap-1.5">
<i class="fa-solid fa-phone"></i>
<span>Accept Call</span>
</button>
</div>
</div>

View File

@ -4,14 +4,17 @@
<div id="notepad-modal" class="fixed top-[70px] right-[360px] w-[420px] h-[350px] bg-slate-900 border-2 border-indigo-500 rounded-2xl shadow-2xl hidden flex-col z-50 overflow-hidden">
<div class="bg-slate-800 px-4 py-2.5 flex justify-between items-center border-b border-white/10">
<div class="font-bold text-xs text-white flex items-center gap-1.5">
<span>📝</span>
<div class="font-bold text-xs text-white flex items-center gap-2">
<i class="fa-solid fa-note-sticky text-indigo-400"></i>
<span>Candidate Notepad (Synced)</span>
</div>
<button type="button" onclick="toggleNotepadModal()" class="bg-transparent border-none text-slate-400 hover:text-white text-lg cursor-pointer leading-none">&times;</button>
<button type="button" onclick="toggleNotepadModal()" class="w-6 h-6 rounded-md bg-slate-700/60 hover:bg-slate-700 text-slate-300 hover:text-white flex items-center justify-center cursor-pointer text-xs transition-colors">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
<textarea id="notepad-textarea" oninput="saveNotepadContent()" placeholder="Type rough notes, pseudo-code, or thoughts here..." class="flex-1 w-full p-3.5 bg-slate-950 text-slate-200 border-none outline-none font-mono text-xs resize-none placeholder-slate-600">{{ $notes }}</textarea>
<div class="px-3 py-1.5 bg-slate-900 text-[10.5px] text-emerald-400 text-right border-t border-white/5 font-medium">
Auto-saved to interviewer portal
<textarea id="notepad-textarea" oninput="saveNotepadContent()" placeholder="Type rough notes, pseudo-code, or thoughts here..." class="flex-1 w-full p-3.5 bg-slate-950 text-slate-200 border-none outline-none font-mono text-xs resize-none placeholder-slate-600 leading-relaxed">{{ $notes }}</textarea>
<div class="px-3 py-1.5 bg-slate-900 text-[10.5px] text-emerald-400 text-right border-t border-white/5 font-medium flex items-center justify-end gap-1.5">
<i class="fa-solid fa-circle-check text-[10px]"></i>
<span>Auto-saved to interviewer portal</span>
</div>
</div>

View File

@ -4,7 +4,7 @@
<aside class="w-[340px] bg-slate-900/90 border-l border-white/10 p-4 flex flex-col gap-4 overflow-y-auto shrink-0 select-none">
<!-- Candidate Camera Feed -->
<div class="w-full h-[180px] min-h-[180px] bg-black rounded-xl border-2 border-white/15 overflow-hidden relative shrink-0">
<div class="w-full aspect-[16/10] bg-black rounded-xl border-2 border-white/15 overflow-hidden relative shrink-0">
<video id="webcam" autoplay muted playsinline class="w-full h-full object-cover -scale-x-100"></video>
<div id="webcam-avatar-placeholder" class="absolute inset-0 hidden flex-col items-center justify-center bg-slate-900 text-white z-10">
<div class="w-14 h-14 rounded-full bg-gradient-to-br from-indigo-500 to-sky-600 flex items-center justify-center text-xl font-bold font-outfit shadow-lg shadow-indigo-500/40">
@ -22,7 +22,7 @@
<!-- Remote Interviewer Panelist Video Grid (Multi-Party WebRTC Mesh) -->
<div id="interviewer-mesh-grid" class="flex flex-col gap-2.5 w-full">
<div id="interviewer-placeholder-box" class="w-full h-[180px] min-h-[180px] bg-black rounded-xl border-2 border-indigo-500/40 overflow-hidden relative shrink-0">
<div id="interviewer-placeholder-box" class="w-full aspect-[16/10] bg-black rounded-xl border-2 border-indigo-500/40 overflow-hidden relative shrink-0">
<video id="interviewer-video-default" autoplay playsinline class="w-full h-full object-cover bg-slate-950 hidden -scale-x-100"></video>
<div id="interviewer-video-placeholder" class="absolute inset-0 flex flex-col items-center justify-center bg-slate-900/95 text-slate-400 text-xs text-center p-2.5 z-10">
<div id="interviewer-avatar-circle" class="w-14 h-14 rounded-full bg-gradient-to-br from-sky-400 to-indigo-500 flex items-center justify-center text-xl font-bold font-outfit text-white mb-1.5 shadow-lg shadow-sky-500/30">
@ -31,8 +31,9 @@
<div id="call-status-sub" class="text-xs text-slate-200 font-semibold">Interviewer Panel</div>
<div class="text-[10.5px] text-sky-400 mt-0.5 font-medium">Waiting for interviewer panelist to join call...</div>
</div>
<div class="absolute bottom-2 left-2 bg-indigo-600/90 px-2 py-0.5 rounded text-[10.5px] text-white font-semibold z-10 backdrop-blur-xs">
🎙️ Interviewer / Panelist
<div class="absolute bottom-2 left-2 bg-indigo-600/90 px-2 py-0.5 rounded text-[10.5px] text-white font-semibold z-10 backdrop-blur-xs flex items-center gap-1">
<i class="fa-solid fa-microphone-lines text-[9px]"></i>
<span>Interviewer / Panelist</span>
</div>
</div>
</div>
@ -42,7 +43,10 @@
<!-- Candidate Profile Info -->
<div class="bg-white/[0.03] border border-white/10 rounded-xl p-3.5 text-xs leading-relaxed">
<div class="font-bold text-white mb-1.5">Candidate Profile</div>
<div class="font-bold text-white mb-1.5 flex items-center gap-1.5">
<i class="fa-solid fa-user-tie text-indigo-400"></i>
<span>Candidate Profile</span>
</div>
<div class="text-slate-400">Name: <strong class="text-white">{{ $interview->candidate_name }}</strong></div>
<div class="text-slate-400">Email: <span class="text-white font-mono text-[11.5px]">{{ $interview->candidate_email }}</span></div>
<div class="text-slate-400 mt-1.5">Access Expires: <span class="text-red-400 font-semibold">{{ $interview->expires_at->format('H:i:s T') }}</span></div>
@ -51,44 +55,52 @@
<!-- Real-Time Call Controls (Mic, Camera, Screen Sharing) -->
<div class="bg-indigo-500/10 border border-indigo-500/30 rounded-xl p-3.5">
<div class="text-xs font-bold text-white mb-2.5 flex items-center justify-between">
<span>📞 Real-Time Interview Call</span>
<span class="flex items-center gap-1.5">
<i class="fa-solid fa-phone text-indigo-400"></i>
<span>Real-Time Interview Call</span>
</span>
<span id="call-status-badge" class="text-[10px] font-bold bg-emerald-500/20 text-emerald-400 px-2 py-0.5 rounded-full">READY</span>
</div>
<button type="button" id="candidate-join-active-call-btn" onclick="acceptCandidateCall()" class="hidden w-full p-2.5 mb-2.5 text-xs font-bold bg-gradient-to-r from-emerald-500 to-emerald-600 text-white rounded-lg border-none shadow-lg shadow-emerald-500/30 cursor-pointer animate-pulse">
🟢 📞 Call in Progress &bull; Join Call Now
<i class="fa-solid fa-phone-volume mr-1.5"></i> Call in Progress &bull; Join Call Now
</button>
<div class="flex gap-2 mb-2.5">
<button type="button" id="toggle-mic-btn" onclick="toggleMic()" class="flex-1 py-2 px-2 text-xs font-semibold rounded-lg bg-emerald-500/20 border border-emerald-500 text-emerald-300 hover:bg-emerald-500/30 transition-all cursor-pointer">
🎤 Mic On
<button type="button" id="toggle-mic-btn" onclick="toggleMic()" class="flex-1 py-2 px-2 text-xs font-semibold rounded-lg bg-emerald-500/20 border border-emerald-500 text-emerald-300 hover:bg-emerald-500/30 transition-all cursor-pointer flex items-center justify-center gap-1.5">
<i class="fa-solid fa-microphone"></i>
<span>Mic On</span>
</button>
<button type="button" id="toggle-cam-btn" onclick="toggleCam()" class="flex-1 py-2 px-2 text-xs font-semibold rounded-lg bg-emerald-500/20 border border-emerald-500 text-emerald-300 hover:bg-emerald-500/30 transition-all cursor-pointer">
📷 Cam On
<button type="button" id="toggle-cam-btn" onclick="toggleCam()" class="flex-1 py-2 px-2 text-xs font-semibold rounded-lg bg-emerald-500/20 border border-emerald-500 text-emerald-300 hover:bg-emerald-500/30 transition-all cursor-pointer flex items-center justify-center gap-1.5">
<i class="fa-solid fa-video"></i>
<span>Cam On</span>
</button>
<button type="button" id="leave-call-btn" onclick="candidateLeaveCall()" class="py-2 px-2.5 text-xs font-semibold rounded-lg bg-red-500/20 border border-red-500 text-red-300 hover:bg-red-500/30 transition-all cursor-pointer" title="Leave 2-Way Call">
🚪 Leave Call
<button type="button" id="leave-call-btn" onclick="candidateLeaveCall()" class="py-2 px-2.5 text-xs font-semibold rounded-lg bg-red-500/20 border border-red-500 text-red-300 hover:bg-red-500/30 transition-all cursor-pointer flex items-center justify-center gap-1" title="Leave 2-Way Call">
<i class="fa-solid fa-phone-slash"></i>
<span>Leave</span>
</button>
</div>
<button type="button" id="share-screen-btn" onclick="startScreenShare()" class="w-full py-2.5 px-3 text-xs font-bold bg-gradient-to-r from-indigo-500 to-sky-600 hover:from-indigo-600 hover:to-sky-700 text-white rounded-lg border-none shadow-md shadow-indigo-500/30 transition-all cursor-pointer">
🖥️ Share Screen with Interviewer
<button type="button" id="share-screen-btn" onclick="startScreenShare()" class="w-full py-2.5 px-3 text-xs font-bold bg-gradient-to-r from-indigo-500 to-sky-600 hover:from-indigo-600 hover:to-sky-700 text-white rounded-lg border-none shadow-md shadow-indigo-500/30 transition-all cursor-pointer flex items-center justify-center gap-1.5">
<i class="fa-solid fa-desktop"></i>
<span>Share Screen with Interviewer</span>
</button>
</div>
<!-- 2-Way Real-Time Chat Container with Interviewer/Panelist -->
<div class="bg-slate-900/80 border border-white/10 rounded-xl p-3">
<div class="text-xs font-bold text-white mb-2 flex items-center gap-1.5">
<span>💬 Live Chat with Interviewer</span>
<i class="fa-solid fa-comments text-indigo-400"></i>
<span>Live Chat with Interviewer</span>
</div>
<div id="candidate-chat-history" class="bg-[#050811] border border-white/10 rounded-lg p-2 h-[130px] overflow-y-auto text-xs mb-2 flex flex-col gap-1">
<div class="text-slate-500 italic text-center pt-10 text-[11px]">No messages yet. Send a message below.</div>
</div>
<div class="flex gap-1.5">
<input type="text" id="candidate-chat-input" placeholder="Type message to panelist..." onkeydown="if(event.key==='Enter') sendCandidateMessage()" class="flex-1 text-xs bg-slate-950 text-white border border-white/10 rounded-lg px-2.5 py-1.5 outline-none focus:border-indigo-500 placeholder-slate-500">
<button type="button" onclick="sendCandidateMessage()" class="px-3 py-1.5 text-xs font-semibold bg-indigo-600 hover:bg-indigo-500 text-white rounded-lg border-none cursor-pointer transition-colors">
<x-button type="button" onclick="sendCandidateMessage()" variant="primary" size="sm" icon="fa-solid fa-paper-plane">
Send
</button>
</x-button>
</div>
</div>
</aside>

View File

@ -1,6 +1,9 @@
<div class="h-[220px] bg-[#050811] border-t border-white/10 flex flex-col shrink-0">
<div class="h-9 bg-[#090e1a] border-b border-white/10 flex items-center justify-between px-4 text-xs font-semibold text-slate-400">
<span class="flex items-center gap-1.5">
<i class="fa-solid fa-terminal text-slate-500"></i>
<span>EXECUTION OUTPUT TERMINAL (STDOUT / STDERR)</span>
</span>
<span id="run-status" class="text-slate-400">Ready</span>
</div>
<div id="terminal-output" class="flex-1 p-3.5 font-['Fira_Code',monospace] text-xs text-emerald-400 overflow-y-auto whitespace-pre-wrap leading-relaxed">// Press 'Run Code' to execute candidate solution live...</div>

View File

@ -0,0 +1,95 @@
@props([
'interview',
])
<details id="inspector-details" class="relative group select-none">
<!-- Summary Header Bar -->
<summary class="list-none [&::-webkit-details-marker]:hidden bg-[#0e1422] border border-[#1b2233] rounded-xl p-3 flex items-center justify-between gap-2 cursor-pointer transition-all hover:border-indigo-500/40">
<div id="inspector-active-tab-title" class="text-xs font-bold text-slate-300 uppercase tracking-wider flex items-center gap-2">
<i id="inspector-active-tab-icon" class="fa-solid fa-layer-group text-indigo-400"></i>
<span id="inspector-active-tab-text">Inspector Panel</span>
</div>
<div class="flex items-center gap-2">
<!-- Button Toolbar -->
<div class="inline-flex items-center gap-1 p-0.5 bg-[#090d16] border border-[#1b2233] rounded-lg">
<button
type="button"
id="tab-btn-logs"
title="Proctoring Logs"
onclick="switchIdeTab('logs', event)"
class="w-7 h-7 flex items-center justify-center rounded-md text-xs font-semibold transition-all cursor-pointer text-slate-400 hover:text-white hover:bg-white/5"
>
<i class="fa-solid fa-shield-halved"></i>
</button>
<button
type="button"
id="tab-btn-notes"
title="Candidate Notepad"
onclick="switchIdeTab('notes', event)"
class="w-7 h-7 flex items-center justify-center rounded-md text-xs font-semibold transition-all cursor-pointer text-slate-400 hover:text-white hover:bg-white/5"
>
<i class="fa-solid fa-note-sticky"></i>
</button>
<button
type="button"
id="tab-btn-drawing"
title="Candidate Drawing Board"
onclick="switchIdeTab('drawing', event)"
class="w-7 h-7 flex items-center justify-center rounded-md text-xs font-semibold transition-all cursor-pointer text-slate-400 hover:text-white hover:bg-white/5"
>
<i class="fa-solid fa-pen"></i>
</button>
<button
type="button"
id="tab-btn-recordings"
title="Saved Video Recordings"
onclick="switchIdeTab('recordings', event)"
class="w-7 h-7 flex items-center justify-center rounded-md text-xs font-semibold transition-all cursor-pointer text-slate-400 hover:text-white hover:bg-white/5"
>
<i class="fa-solid fa-film"></i>
</button>
<button
type="button"
id="tab-btn-screenshots"
title="Tab Switch Screenshots"
onclick="switchIdeTab('screenshots', event)"
class="w-7 h-7 flex items-center justify-center rounded-md text-xs font-semibold transition-all cursor-pointer text-slate-400 hover:text-white hover:bg-white/5"
>
<i class="fa-solid fa-image"></i>
</button>
</div>
</div>
</summary>
<!-- Floating Inspector Content Overlay -->
<div id="inspector-content-container" class="absolute top-[calc(100%+8px)] right-0 left-0 lg:left-auto lg:w-112.5 bg-[#0e1422] border border-[#1b2233] rounded-xl p-3 shadow-2xl shadow-black/80 backdrop-blur-md z-50 flex flex-col max-h-125 overflow-hidden">
<!-- Floating Header -->
<div class="flex items-center justify-between mb-2">
<div id="inspector-floating-title" class="text-xs font-bold text-slate-200 uppercase tracking-wider flex items-center gap-2">
<i id="inspector-floating-icon" class="fa-solid fa-shield-halved text-indigo-400"></i>
<span id="inspector-floating-text">Proctoring Logs</span>
</div>
<button
type="button"
id="inspector-close-btn"
title="Close Inspector"
onclick="closeInspectorPanel()"
class="w-6 h-6 rounded-md bg-white/5 hover:bg-white/15 text-slate-400 hover:text-white flex items-center justify-center cursor-pointer text-xs transition-colors"
>
<i class="fa-solid fa-xmark"></i>
</button>
</div>
<!-- Floating Tab Content Panes -->
<div class="flex-1 overflow-y-auto">
<div id="tab-content-logs" class="hidden">
<x-audit-log id="modal-logs-display" :logs="$interview->proctor_logs ?? []" class="h-75 overflow-y-auto" />
</div>
<x-interview.tabs.notes-tab />
<x-interview.tabs.drawing-tab />
<x-interview.tabs.recordings-tab />
<x-interview.tabs.screenshots-tab />
</div>
</div>
</details>

View File

@ -2,9 +2,9 @@
'activeNav' => 'interview',
])
<header class="h-[70px] bg-slate-900/80 backdrop-blur-md border-b border-slate-700/60 flex items-center justify-between px-6 sticky top-0 z-50">
<header class="h-17.5 bg-slate-900/80 backdrop-blur-md border-b border-slate-700/60 flex items-center justify-between px-6">
<div class="flex items-center gap-3">
<a href="{{ route('interview.index') }}" class="w-9 h-9 rounded-lg bg-gradient-to-br from-indigo-500 to-sky-600 flex items-center justify-center font-bold text-white shadow-md shadow-indigo-500/30 no-underline">
<a href="{{ route('interview.index') }}" class="w-9 h-9 rounded-lg bg-linear-to-br from-indigo-500 to-sky-600 flex items-center justify-center font-bold text-white shadow-md shadow-indigo-500/30 no-underline">
<i class="fa-solid fa-bolt text-lg"></i>
</a>
<div>

View File

@ -0,0 +1,82 @@
@props([
'interviewers' => [],
])
<x-drawer
id="create-interview-drawer"
title="Schedule Candidate Assessment"
description="Provision candidate temporary credentials, IDE access timeline, and assign review panelists."
width="lg"
>
<form action="{{ route('interview.store') }}" method="POST">
@csrf
<div class="space-y-4">
<div>
<x-label required>Candidate Name</x-label>
<x-input type="text" name="candidate_name" required placeholder="e.g. John Doe" />
</div>
<div>
<x-label required>Candidate Email</x-label>
<x-input type="email" name="candidate_email" required placeholder="john@gmail.com" />
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3.5">
<div>
<x-label required>Phone Number</x-label>
<x-input type="text" name="candidate_phone" required placeholder="9876543210" />
</div>
<div>
<x-label required>Access Duration</x-label>
<x-select name="valid_hours" required>
<option value="1">1 Hour</option>
<option value="2" selected>2 Hours</option>
<option value="4">4 Hours</option>
<option value="12">12 Hours</option>
<option value="24">24 Hours</option>
</x-select>
</div>
</div>
<div>
<x-label>Start Time (Timeline)</x-label>
<x-input type="datetime-local" name="scheduled_at" value="{{ now()->format('Y-m-d\TH:i') }}" class="bg-slate-900" />
</div>
<div>
<x-label required>Coding Language</x-label>
<x-select name="language" required>
<option value="python">Python 3</option>
<option value="cpp">C++ (GCC)</option>
<option value="c">C (GCC)</option>
<option value="java">Java 15</option>
<option value="php">PHP 8.2 / Laravel</option>
<option value="javascript">JavaScript (Node.js)</option>
</x-select>
</div>
<div>
<div class="flex justify-between items-center mb-1.5">
<x-label>Assign Internal Interviewers / Panelists</x-label>
</div>
<div class="space-y-2 max-h-[160px] overflow-y-auto bg-black/30 p-3 rounded-xl border border-slate-700/60">
@foreach($interviewers as $usr)
<label class="flex items-center gap-2.5 text-xs text-white cursor-pointer hover:text-indigo-300 transition-colors py-0.5">
<input type="checkbox" name="assigned_interviewers[]" value="{{ $usr->id }}" class="accent-indigo-500 rounded w-4 h-4">
<span>{{ $usr->name }} <span class="text-slate-400 font-mono text-[11px]">({{ $usr->email }})</span></span>
</label>
@endforeach
</div>
</div>
</div>
<div class="flex justify-end gap-2.5 pt-6 mt-6 border-t border-white/10">
<x-button type="button" onclick="document.getElementById('create-interview-drawer').classList.remove('active')" variant="secondary">
Cancel
</x-button>
<x-button type="submit" variant="primary" icon="fa-solid fa-check">
Create Session & Password
</x-button>
</div>
</form>
</x-drawer>

View File

@ -21,7 +21,7 @@
'focused' => false,
])
<div id="{{ $id }}" {{ $attributes->merge(['class' => 'video-tile relative aspect-[16/11] bg-[#0d1220] border rounded-[10px] overflow-hidden flex flex-col items-center justify-center transition-all ' . ($focused ? 'border-[rgba(75,130,247,0.35)] shadow-[0_0_0_1px_rgba(75,130,247,0.35)]' : 'border-[#1b2233]')]) }}>
<div id="{{ $id }}" {{ $attributes->twMerge(['class' => 'video-tile relative aspect-[16/10] bg-[#0d1220] border rounded-[10px] overflow-hidden flex flex-col items-center justify-center transition-all ' . ($focused ? 'border-[rgba(75,130,247,0.35)] shadow-[0_0_0_1px_rgba(75,130,247,0.35)]' : 'border-[#1b2233]')]) }}>
@if($showZoom || $showFullscreen)
<div class="tile-tools absolute top-2 right-2 z-20 flex gap-1">
@if($showZoom)
@ -44,16 +44,16 @@
@endif
<div id="{{ $placeholderId }}" class="absolute inset-0 flex flex-col items-center justify-center bg-[#0d1220]/95 text-slate-400 text-xs z-10 p-3">
<div id="{{ $avatarCircleId }}" class="av w-[58px] h-[58px] rounded-full bg-gradient-to-br from-[#5b8bff] to-[#3b63e0] flex items-center justify-center text-white font-semibold text-[19px] mb-2.5 shadow-md shadow-[#4b82f7]/20">
<div id="{{ $avatarCircleId }}" class="av w-8 h-8 rounded-full bg-linear-to-br from-[#5b8bff] to-[#3b63e0] flex items-center justify-center text-white font-semibold text-xs mb-2.5 shadow-md shadow-[#4b82f7]/20">
{{ $initials }}
</div>
<div id="{{ $avatarNameId }}" class="name text-[13.5px] font-semibold text-white">{{ $avatarTitle }}</div>
<div class="status text-[11.5px] text-[#5b637a] mt-1 text-center px-3">
<div id="{{ $avatarNameId }}" class="name text-xs font-semibold text-white">{{ $avatarTitle }}</div>
<div class="status text-[8px] text-[#5b637a] mt-1 text-center px-3">
{{ $slot->isNotEmpty() ? $slot : "Waiting for candidate&hellip;" }}
</div>
</div>
<span class="tile-tag absolute left-2.5 bottom-2.5 text-[10px] font-semibold tracking-wide px-2.5 py-1 rounded-md bg-black/50 text-slate-300 border border-[#1b2233] z-20 flex items-center gap-1.5 backdrop-blur-sm">
<span class="tile-tag absolute left-1 bottom-1 text-[9px] font-semibold tracking-wide px-2.5 py-1 rounded-md bg-black/50 text-slate-300 border border-[#1b2233] z-20 flex items-center gap-1.5 backdrop-blur-sm">
<span>{{ $badgeText }}</span>
@if($micStatusId || $camStatusId)
<span class="w-px h-2.5 bg-white/20 mx-0.5"></span>

View File

@ -9,8 +9,8 @@
<div class="text-xs text-slate-400 mt-0.5">Temporary Candidate Access, Real-Time Proctored IDE, and Unique Code Records</div>
</div>
<x-button onclick="document.getElementById('create-interview-modal').classList.add('active')" variant="primary" size="md" icon="fa-solid fa-plus">
Schedule Candidate Interview
<x-button onclick="document.getElementById('create-interview-drawer').classList.add('active')" variant="primary" size="md" icon="fa-solid fa-plus">
Schedule Candidate Assessment
</x-button>
</div>
@ -34,7 +34,7 @@
@empty
<tr>
<td colspan="6" class="text-center text-slate-400 py-8">
No candidate interview sessions found. Click "+ Schedule Candidate Interview" above to create one.
No candidate interview sessions found. Click "+ Schedule Candidate Assessment" above to create one.
</td>
</tr>
@endforelse
@ -43,6 +43,6 @@
</div>
</x-card>
<!-- Modal: Schedule Candidate Interview -->
<x-interview.schedule-modal :interviewers="$interviewers" />
<!-- Drawer: Schedule Candidate Assessment -->
<x-interview.schedule-drawer :interviewers="$interviewers" />
</x-layouts.interview>

View File

@ -17,9 +17,13 @@
<x-interview.call-controls :interview="$interview" />
</x-slot:headerExtra>
<!-- Video Grid -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-3.5 p-5">
<!-- Candidate Video -->
<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"
@ -37,8 +41,10 @@
initials="{{ $interview->candidate_initials }}">
<span id="cand-status-text">Waiting for candidate&hellip;</span>
</x-video-tile>
</div>
<!-- Candidate Shared Screen -->
<!-- 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"
@ -56,12 +62,55 @@
initials="CD">
Waiting for candidate to share screen&hellip;
</x-video-tile>
</div>
</div>
</div>
<!-- Panelist Self -->
<!-- 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-3 overflow-x-auto pb-1.5">
<!-- 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"
@ -75,48 +124,8 @@
</x-video-tile>
</div>
<!-- Multi-Party Panelist Video Grid -->
<div id="panelist-mesh-grid" class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-3.5 px-5 pb-5"></div>
</x-card>
<!-- Inspector Panel (Tabs & Audit Log / Live Chat) -->
<x-card>
<div class="inline-flex gap-1 p-1 bg-[#0d1220] border border-[#1b2233] rounded-[10px] mx-5 mt-4">
<x-tab-button id="tab-btn-code" tabKey="code" active="true" icon="fa-solid fa-code" onclick="switchIdeTab('code')">Live IDE code</x-tab-button>
<x-tab-button id="tab-btn-notes" tabKey="notes" active="false" icon="fa-solid fa-note-sticky" onclick="switchIdeTab('notes')">Candidate notepad</x-tab-button>
<x-tab-button id="tab-btn-drawing" tabKey="drawing" active="false" icon="fa-solid fa-pen" onclick="switchIdeTab('drawing')">Candidate drawing</x-tab-button>
<x-tab-button id="tab-btn-recordings" tabKey="recordings" active="false" icon="fa-solid fa-film" onclick="switchIdeTab('recordings')">Saved recordings</x-tab-button>
<x-tab-button id="tab-btn-screenshots" tabKey="screenshots" active="false" icon="fa-solid fa-image" onclick="switchIdeTab('screenshots')">Tab switch screenshots</x-tab-button>
</div>
<div class="grid grid-cols-1 lg:grid-cols-5 gap-4 p-5">
<!-- Left 3 Cols: Tab Content Panes -->
<div class="lg:col-span-3">
<x-interview.tabs.code-tab />
<x-interview.tabs.notes-tab />
<x-interview.tabs.drawing-tab />
<x-interview.tabs.recordings-tab />
<x-interview.tabs.screenshots-tab />
</div>
<!-- Right 2 Cols: Audit Log & Chat -->
<div class="lg:col-span-2">
<h3 class="text-[12.5px] font-semibold text-white mb-2.5 flex items-center gap-2 m-0">
Proctoring logs
</h3>
<x-audit-log id="modal-logs-display" :logs="$interview->proctor_logs ?? []" />
<div class="mt-4">
<h3 class="text-[12.5px] font-semibold text-white mb-2.5 flex items-center gap-2 m-0">
Live Chat
</h3>
<x-chat-panel id="interviewer-chat-history" class="mb-2.5" :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-[#0d1220] border border-[#232b3d] rounded-lg px-3 py-2 text-white text-xs outline-none focus:border-[rgba(75,130,247,0.35)] placeholder-[#5b637a]">
<x-button onclick="sendSilentWarning()" variant="primary" icon="fa-solid fa-paper-plane">Send</x-button>
</div>
</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>
@ -126,6 +135,80 @@
@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 }},
@ -133,6 +216,14 @@
"{{ $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