feat: UI Refactor, multi-people recording and noise cancellation #21
@ -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
143
composer.lock
generated
@ -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",
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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';
|
||||
}
|
||||
|
||||
@ -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 => {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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"
|
||||
|
||||
61
resources/views/components/drawer.blade.php
Normal file
61
resources/views/components/drawer.blade.php
Normal 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>
|
||||
@ -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 & 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 & 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">
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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">×</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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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">×</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>
|
||||
|
||||
@ -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 • Join Call Now
|
||||
<i class="fa-solid fa-phone-volume mr-1.5"></i> Call in Progress • 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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
@ -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…" }}
|
||||
</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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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…</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…
|
||||
</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…" 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…" 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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user