testing mode going on

This commit is contained in:
subhajit 2026-07-21 17:02:47 +05:30
parent 5b61495e35
commit 0924d1c546

View File

@ -39,8 +39,8 @@
}
::-webkit-scrollbar-thumb {
background-color: rgba(255, 255, 255, 0.4);
border-radius: 9999px;
background: rgba(255, 255, 255, 0.4);
border-radius: 10px;
}
* {
@ -52,95 +52,82 @@
body {
font-family: 'Instrument Sans', sans-serif;
background-color: var(--bg-color);
background-image: radial-gradient(circle at 85% 15%, rgba(99, 102, 241, 0.12) 0%, transparent 45%),
radial-gradient(circle at 15% 85%, rgba(0, 120, 212, 0.12) 0%, transparent 45%);
background-attachment: fixed;
color: var(--text-main);
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* Top Navbar */
.navbar {
height: 70px;
background: rgba(15, 23, 42, 0.8);
backdrop-filter: blur(16px);
border-bottom: 1px solid var(--card-border);
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 5%;
background: rgba(10, 15, 26, 0.65);
border-bottom: 1px solid var(--card-border);
backdrop-filter: blur(20px);
padding: 0 4%;
position: sticky;
top: 0;
z-index: 100;
}
.btn-nav {
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--card-border);
color: white;
padding: 8px 16px;
border-radius: 10px;
color: #e5e7eb;
text-decoration: none;
font-size: 0.85rem;
font-weight: 500;
cursor: pointer;
text-decoration: none;
transition: all 0.2s ease;
padding: 8px 16px;
border-radius: 8px;
border: 1px solid var(--card-border);
background: rgba(255, 255, 255, 0.03);
transition: all 0.25s ease;
}
.btn-nav:hover {
background: rgba(255, 255, 255, 0.12);
border-color: rgba(255, 255, 255, 0.3);
transform: translateY(-1px);
background: rgba(255, 255, 255, 0.08);
color: white;
}
/* Assessment Card */
.assessment-card {
background: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: 20px;
padding: 24px;
backdrop-filter: blur(16px);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4);
padding: 28px;
backdrop-filter: blur(20px);
box-shadow: 0 20px 40px rgba(0,0,0,0.4);
margin-bottom: 24px;
}
/* Users Table */
.table-responsive {
width: 100%;
overflow-x: auto;
width: 100%;
}
.users-table {
width: 100%;
border-collapse: collapse;
font-size: 0.85rem;
text-align: left;
font-size: 0.9rem;
}
.users-table th {
padding: 14px 16px;
background: rgba(255, 255, 255, 0.03);
color: var(--text-muted);
font-weight: 600;
padding: 12px 16px;
border-bottom: 1px solid var(--card-border);
font-size: 0.8rem;
color: var(--text-muted);
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.users-table td {
padding: 16px;
padding: 14px 16px;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
vertical-align: middle;
}
.users-table tr:hover td {
background: rgba(255, 255, 255, 0.02);
}
.badge {
display: inline-block;
display: inline-flex;
align-items: center;
padding: 4px 8px;
border-radius: 9999px;
font-size: 0.7rem;
@ -174,57 +161,36 @@
border: 1px solid var(--card-border);
border-radius: 20px;
width: 90%;
max-width: 1150px;
padding: 24px;
max-width: 900px;
padding: 28px;
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.7);
max-height: 92vh;
color: white;
max-height: 90vh;
overflow-y: auto;
}
.form-group {
margin-bottom: 16px;
}
.form-label {
display: block;
font-size: 0.8rem;
font-weight: 600;
color: var(--text-muted);
margin-bottom: 6px;
}
.form-input {
width: 100%;
padding: 10px 14px;
background: rgba(0, 0, 0, 0.3);
padding: 10px 12px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--card-border);
border-radius: 10px;
border-radius: 8px;
color: white;
font-size: 0.85rem;
outline: none;
}
.form-input:focus {
border-color: var(--accent-primary);
}
@keyframes sosPulse {
0% { transform: scale(1); box-shadow: 0 0 10px rgba(239,68,68,0.7); }
100% { transform: scale(1.05); box-shadow: 0 0 25px rgba(239,68,68,1); }
}
</style>
</head>
<body>
<!-- Header Navbar -->
<header class="navbar">
<div style="display: flex; align-items: center; gap: 12px;">
<div style="width: 38px; height: 38px; border-radius: 10px; background: linear-gradient(135deg, #6366f1 0%, #0078d4 100%); display: flex; align-items: center; justify-content: center; font-weight: 800; font-family: 'Outfit', sans-serif;">
SL
<div style="width: 36px; height: 36px; border-radius: 10px; background: linear-gradient(135deg, #6366f1 0%, #0078d4 100%); display: flex; align-items: center; justify-content: center; font-weight: 700;">
</div>
<div>
<h1 style="font-family: 'Outfit', sans-serif; font-size: 1.15rem; font-weight: 700; color: white; margin: 0;">Interview Zone & Assessment Hub</h1>
<div style="font-size: 0.75rem; color: var(--text-muted);">Proctored IDE, Live Workspace Inspector & 2-Way Calls</div>
<h1 style="font-family: 'Outfit', sans-serif; font-size: 1.15rem; font-weight: 700; color: white; margin: 0;">Candidate Live Assessment Portal</h1>
<div style="font-size: 0.7rem; color: var(--text-muted);">Proctored IDE & Code Submissions</div>
</div>
</div>
@ -284,31 +250,53 @@
@endphp
<tr>
<td>
<div style="font-weight: 600; color: white;">{{ $inv->candidate_name }}</div>
<div style="font-size: 0.75rem; color: #38bdf8;">{{ $inv->candidate_email }}</div>
<div style="font-size: 0.7rem; color: #fcd34d; margin-top: 2px;">Pass: <code>{{ $inv->temp_password }}</code></div>
<div>
<strong style="color: white; font-size: 0.9rem;">{{ $inv->candidate_name }}</strong>
<div style="font-size: 0.75rem; color: var(--text-muted);">{{ $inv->candidate_email }} &bull; {{ $inv->candidate_phone }}</div>
<div style="margin-top: 4px; display: flex; flex-direction: column; gap: 4px;">
<div style="font-size: 0.75rem; color: #a7f3d0; background: rgba(16,185,129,0.1); padding: 2px 8px; border-radius: 6px; display: inline-block;">
🔑 Temp Pass: <strong>{{ $inv->temp_password }}</strong>
</div>
<div style="font-size: 0.7rem; color: #38bdf8;">
🌐 Login URL: <a href="{{ route('interview.candidate.login') }}" target="_blank" style="color: #38bdf8; text-decoration: underline;">{{ route('interview.candidate.login') }}</a>
</div>
</div>
</div>
</td>
<td>
<span style="font-family: monospace; font-size: 0.75rem; background: rgba(0,0,0,0.3); padding: 4px 8px; border-radius: 6px; border: 1px solid var(--card-border); color: #a7f3d0;">
<code style="background: rgba(255,255,255,0.06); padding: 4px 8px; border-radius: 6px; color: #38bdf8; font-size: 0.75rem;">
{{ $inv->submission_unique_id }}
</span>
</code>
</td>
<td>
@if($inv->isExpired())
<span class="badge" style="background: rgba(239, 68, 68, 0.2); color: #fca5a5;">EXPIRED</span>
@elseif($inv->status === 'submitted')
<span class="badge" style="background: rgba(16, 185, 129, 0.2); color: #34d399;">SUBMITTED</span>
<span class="badge" style="background: rgba(239,68,68,0.15); color: #fca5a5; border: 1px solid rgba(239,68,68,0.3);">
EXPIRED
</span>
@elseif($inv->status === 'completed')
<span class="badge" style="background: rgba(16,185,129,0.15); color: #34d399; border: 1px solid rgba(16,185,129,0.3);">
COMPLETED
</span>
@elseif($inv->isActiveTimeline())
<span class="badge" style="background: rgba(16,185,129,0.2); color: #34d399; border: 1px solid rgba(16,185,129,0.4);">
TIMELINE LIVE
</span>
@else
<span class="badge" style="background: rgba(56, 189, 248, 0.2); color: #38bdf8;">ACTIVE ({{ $inv->expires_at->diffForHumans() }})</span>
<span class="badge" style="background: rgba(245,158,11,0.15); color: #fbbf24; border: 1px solid rgba(245,158,11,0.3);">
SCHEDULED
</span>
@endif
<div style="font-size: 0.7rem; color: var(--text-muted); margin-top: 4px;">
Timeline: {{ ($inv->scheduled_at ?? $inv->created_at)->format('M d, H:i') }} &ndash; {{ $inv->expires_at->format('H:i') }}
</div>
</td>
<td>
<span class="badge" style="background: rgba(99, 102, 241, 0.2); color: #a5b4fc;">
{{ strtoupper($inv->language) }}
<span class="badge" style="background: rgba(255,255,255,0.05); color: white; border: 1px solid var(--card-border); text-transform: uppercase;">
{{ $inv->language }}
</span>
</td>
<td>
<div style="display: flex; flex-direction: column; gap: 2px; font-size: 0.7rem;">
<div style="display: flex; flex-direction: column; gap: 2px; font-size: 0.75rem;">
<span style="color: {{ $tabSwitches > 0 ? '#ef4444' : 'var(--text-muted)' }}; font-weight: {{ $tabSwitches > 0 ? '700' : '400' }};">
🔴 Tab Switches: {{ $tabSwitches }}
</span>
@ -337,7 +325,7 @@
@empty
<tr>
<td colspan="6" style="text-align: center; color: var(--text-muted); padding: 30px;">
No candidate interviews scheduled yet.
No candidate interview sessions found. Click "+ Schedule Candidate Interview" above to create one.
</td>
</tr>
@endforelse
@ -347,29 +335,51 @@
</div>
</main>
<!-- Modal: Schedule New Candidate Interview -->
<!-- Modal: Schedule Candidate Interview -->
<div id="create-interview-modal" class="admin-modal">
<div class="admin-modal-content" style="max-width: 520px;">
<div class="admin-modal-content" style="max-width: 600px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h3 style="font-family: 'Outfit', sans-serif; font-size: 1.2rem; font-weight: 700; color: white;">Schedule Candidate Assessment</h3>
<h3 style="font-family: 'Outfit', sans-serif; font-size: 1.25rem; font-weight: 700; color: white; margin: 0;">Schedule Candidate Assessment</h3>
<button onclick="document.getElementById('create-interview-modal').classList.remove('active')" style="background:none; border:none; color:var(--text-muted); font-size:1.5rem; cursor:pointer;">&times;</button>
</div>
<form action="{{ route('interview.store') }}" method="POST">
@csrf
<div class="form-group">
<label class="form-label">Candidate Name</label>
<input type="text" name="candidate_name" required class="form-input" placeholder="e.g. John Doe">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-bottom: 14px;">
<div>
<label style="display:block; font-size:0.75rem; color:var(--text-muted); margin-bottom:4px; font-weight:500;">Candidate Name <span style="color:#ef4444;">*</span></label>
<input type="text" name="candidate_name" required placeholder="e.g. John Doe" class="form-input">
</div>
<div>
<label style="display:block; font-size:0.75rem; color:var(--text-muted); margin-bottom:4px; font-weight:500;">Candidate Email <span style="color:#ef4444;">*</span></label>
<input type="email" name="candidate_email" required placeholder="john@gmail.com" class="form-input">
</div>
</div>
<div class="form-group">
<label class="form-label">Candidate Email</label>
<input type="email" name="candidate_email" required class="form-input" placeholder="e.g. john@example.com">
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px; margin-bottom: 14px;">
<div>
<label style="display:block; font-size:0.75rem; color:var(--text-muted); margin-bottom:4px; font-weight:500;">Phone Number <span style="color:#ef4444;">*</span></label>
<input type="text" name="candidate_phone" required placeholder="9876543210" class="form-input">
</div>
<div>
<label style="display:block; font-size:0.75rem; color:var(--text-muted); margin-bottom:4px; font-weight:500;">Start Time (Timeline)</label>
<input type="datetime-local" name="scheduled_at" value="{{ now()->format('Y-m-d\TH:i') }}" class="form-input" style="background: #151b2c;">
</div>
<div>
<label style="display:block; font-size:0.75rem; color:var(--text-muted); margin-bottom:4px; font-weight:500;">Access Duration <span style="color:#ef4444;">*</span></label>
<select name="valid_hours" required class="form-input" style="background: #151b2c;">
<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>
</select>
</div>
</div>
<div class="form-group">
<label class="form-label">Assessment Programming Language</label>
<select name="language" class="form-input">
<div style="margin-bottom: 14px;">
<label style="display:block; font-size:0.75rem; color:var(--text-muted); margin-bottom:4px; font-weight:500;">Coding Language <span style="color:#ef4444;">*</span></label>
<select name="language" required class="form-input" style="background: #151b2c;">
<option value="python">Python 3</option>
<option value="cpp">C++ (GCC)</option>
<option value="c">C (GCC)</option>
@ -379,14 +389,21 @@
</select>
</div>
<div class="form-group">
<label class="form-label">Access Duration (Hours)</label>
<input type="number" name="duration_hours" value="2" min="1" max="24" class="form-input">
<div style="margin-bottom: 16px;">
<label style="display:block; font-size:0.75rem; color:var(--text-muted); margin-bottom:6px; font-weight:500;">Assign Internal Interviewers / Panelists</label>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; max-height: 120px; overflow-y: auto; background: rgba(0,0,0,0.3); padding: 10px; border-radius: 8px; border: 1px solid var(--card-border);">
@foreach($interviewers as $usr)
<label style="display: flex; align-items: center; gap: 6px; font-size: 0.75rem; color: white; cursor: pointer;">
<input type="checkbox" name="assigned_interviewers[]" value="{{ $usr->id }}" style="accent-color: #6366f1;">
{{ $usr->name }} ({{ $usr->email }})
</label>
@endforeach
</div>
</div>
<div style="display: flex; justify-content: flex-end; gap: 10px; margin-top: 24px;">
<div style="display: flex; justify-content: flex-end; gap: 10px; margin-top: 20px;">
<button type="button" onclick="document.getElementById('create-interview-modal').classList.remove('active')" class="btn-nav">Cancel</button>
<button type="submit" class="btn-nav" style="background: var(--accent-primary); border-color: transparent; font-weight: 600;">Schedule & Generate Pass</button>
<button type="submit" class="btn-nav" style="background: var(--accent-primary); color: white; border-color: transparent; font-weight: 600;">Create Session & Password</button>
</div>
</form>
</div>
@ -402,15 +419,15 @@
</div>
<div style="display: flex; gap: 8px; align-items: center;">
<!-- SOS AUTO TRIGGER TOGGLE BUTTON FOR SESSION -->
<button id="toggle-auto-sos-btn" onclick="toggleAutoSosTrigger()" class="btn-nav" style="background: rgba(239,68,68,0.2); border-color: #ef4444; color: #fca5a5; font-size: 0.75rem; font-weight: 600;">
<!-- TOGGLE SOS AUTO-TRIGGER ON/OFF FOR SESSION -->
<button id="sos-toggle-btn" onclick="toggleSosAutoTrigger()" class="btn-nav" style="background: rgba(99,102,241,0.15); color: #818cf8; border-color: #6366f1; font-size: 0.75rem; font-weight: 600;" title="Panelists can turn off automatic SOS popups for this session">
🔔 SOS Auto-Trigger: ON
</button>
<!-- PDF REPORT DOWNLOAD LINK -->
<a id="btn-modal-pdf-report" href="#" target="_blank" class="btn-nav" style="background: rgba(16,185,129,0.2); border-color: #10b981; color: #34d399; font-size: 0.75rem; font-weight: 600; text-decoration: none;">
<!-- GENERATE PDF REPORT BUTTON -->
<button onclick="openReportPage()" class="btn-nav" style="background: rgba(16,185,129,0.15); color: #34d399; border-color: #10b981; font-size: 0.75rem; font-weight: 600;">
📄 Generate PDF Report
</a>
</button>
<!-- SOS CHEATING ALERT BUTTON (Flashing Red) -->
<button id="sos-alert-btn" onclick="openSosModal()" class="btn-nav" style="display: none; background: linear-gradient(135deg, #ef4444 0%, #b91c1c 100%); color: white; border: none; font-weight: 800; padding: 6px 14px; box-shadow: 0 0 15px rgba(239,68,68,0.7); cursor: pointer; animation: sosPulse 1.2s infinite alternate;">
@ -475,7 +492,7 @@
CD
</div>
<div id="cand-avatar-name" style="font-weight: 700; color: white;">Candidate Camera</div>
<div style="font-size: 0.65rem; color: #38bdf8; margin-top: 2px;">Click 'Start / Join Call' to connect</div>
<div style="font-size: 0.65rem; color: #38bdf8; margin-top: 2px;">Click 'Start / Join 2-Way Call' to connect</div>
</div>
<div style="position: absolute; bottom: 6px; left: 6px; background: rgba(0,0,0,0.6); padding: 2px 8px; border-radius: 4px; font-size: 0.65rem; color: #34d399; font-weight: 600; z-index: 10;">
👤 Candidate Video <span id="cand-vid-zoom-badge" style="color: #38bdf8; font-size: 0.6rem;">(100%)</span>
@ -526,42 +543,43 @@
<div style="font-size: 0.65rem; color: #34d399; margin-top: 2px;">Panelist (Self)</div>
</div>
<div style="position: absolute; bottom: 6px; left: 6px; background: rgba(99,102,241,0.8); padding: 2px 8px; border-radius: 4px; font-size: 0.65rem; color: white; font-weight: 600; z-index: 10;">
🎤 Panelist Self
🎙️ Panelist Self
</div>
</div>
</div>
</div>
<!-- CANDIDATE LIVE IDE & WORKSPACE INSPECTOR -->
<div style="background: rgba(15,23,42,0.8); border: 1.5px solid var(--card-border); border-radius: 16px; padding: 16px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
<div style="display: flex; align-items: center; gap: 8px;">
<span style="font-size: 1.1rem;">💻</span>
<strong style="color: white; font-size: 0.95rem;">Candidate Live IDE & Workspace Inspector</strong>
<span id="modal-lang-badge" class="badge" style="background: rgba(99,102,241,0.2); color: #a5b4fc;">UNKNOWN</span>
<span style="font-size: 0.75rem; color: #34d399; font-weight: 500;"> Live Synced</span>
<!-- LARGE CARD: CANDIDATE IDE & SYNCED NOTEBOOK / DRAWING INSPECTOR (Visible by default even if Screen Share is OFF) -->
<div style="background: #090d16; border: 1.5px solid var(--accent-primary); border-radius: 16px; padding: 20px; margin-bottom: 20px; box-shadow: 0 10px 30px rgba(0,0,0,0.5);">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 14px; border-bottom: 1px solid rgba(255,255,255,0.08); padding-bottom: 10px;">
<div style="display: flex; align-items: center; gap: 10px;">
<span style="font-size: 1.2rem;">💻</span>
<strong style="color: white; font-size: 1.05rem;">Candidate Live IDE & Workspace Inspector</strong>
<span id="modal-code-lang" class="badge" style="background: rgba(99,102,241,0.2); color: #818cf8; font-size: 0.7rem;">PYTHON</span>
<span style="font-size: 0.7rem; color: #34d399;"> Live Synced</span>
</div>
<!-- Tab Switcher for Code Solution, Notepad, Drawing -->
<div style="display: flex; gap: 6px;">
<button onclick="switchInspectorTab('code')" id="tab-btn-code" class="btn-nav" style="background: #6366f1; border-color: transparent;">💻 Live IDE Code</button>
<button onclick="switchInspectorTab('notes')" id="tab-btn-notes" class="btn-nav">📝 Candidate Notepad</button>
<button onclick="switchInspectorTab('drawing')" id="tab-btn-drawing" class="btn-nav">🎨 Candidate Drawing</button>
<button id="tab-btn-code" onclick="switchIdeTab('code')" class="btn-nav" style="padding: 5px 14px; font-size: 0.75rem; background: #6366f1; color: white; border: none; font-weight: 700;">💻 Live IDE Code</button>
<button id="tab-btn-notes" onclick="switchIdeTab('notes')" class="btn-nav" style="padding: 5px 14px; font-size: 0.75rem; background: rgba(255,255,255,0.05); color: #94a3b8; border: 1px solid var(--card-border);">📝 Candidate Notepad</button>
<button id="tab-btn-drawing" onclick="switchIdeTab('drawing')" class="btn-nav" style="padding: 5px 14px; font-size: 0.75rem; background: rgba(255,255,255,0.05); color: #94a3b8; border: 1px solid var(--card-border);">🎨 Candidate Drawing</button>
</div>
</div>
<div style="display: grid; grid-template-columns: 2fr 1fr; gap: 16px; min-height: 280px;">
<!-- Left Column: Code, Output, Notes & Canvas Tabs -->
<div style="display: grid; grid-template-columns: 2.5fr 1fr; gap: 20px;">
<!-- Left Column: Tab Content (Code Solution, Notepad, or Canvas Drawing) -->
<div>
<!-- Tab 1: Live Code & Stdout -->
<div id="tab-content-code">
<div style="font-size: 0.75rem; color: #94a3b8; margin-bottom: 6px; font-weight: 600;">Current Code Typed by Candidate:</div>
<pre id="modal-code-display" style="background: #050811; border: 1px solid var(--card-border); border-radius: 10px; padding: 14px; font-family: monospace; font-size: 0.85rem; color: #a7f3d0; min-height: 160px; max-height: 240px; overflow-y: auto; white-space: pre-wrap; margin-bottom: 10px;"></pre>
<!-- Tab 1: Live Code Solution & Terminal Output -->
<div id="tab-content-code" style="display: block;">
<div style="font-size: 0.75rem; color: #94a3b8; margin-bottom: 6px; font-weight: 600;">Current Code Solution (Visible even if candidate does not share screen):</div>
<pre id="modal-code-display" style="background: #050811; border: 1px solid var(--card-border); border-radius: 10px; padding: 16px; font-family: monospace; font-size: 0.85rem; color: #38bdf8; min-height: 200px; max-height: 280px; overflow-y: auto; white-space: pre-wrap; margin-bottom: 12px;"></pre>
<div style="font-size: 0.75rem; color: #94a3b8; margin-bottom: 4px; font-weight: 600;">Execution Output (Stdout/Stderr):</div>
<pre id="modal-output-display" style="background: #050811; border: 1px solid var(--card-border); border-radius: 10px; padding: 10px; font-family: monospace; font-size: 0.75rem; color: #cbd5e1; max-height: 100px; overflow-y: auto; white-space: pre-wrap; margin: 0;"></pre>
<div style="font-size: 0.75rem; color: #94a3b8; margin-bottom: 6px; font-weight: 600;">Execution Stdout / Stderr Output:</div>
<pre id="modal-output-display" style="background: #02040a; border: 1px solid var(--card-border); border-radius: 10px; padding: 12px; font-family: monospace; font-size: 0.75rem; color: #34d399; max-height: 90px; overflow-y: auto; white-space: pre-wrap;"></pre>
</div>
<!-- Tab 2: Candidate Live Notepad Notes -->
<!-- Tab 2: Candidate Notepad Content -->
<div id="tab-content-notes" style="display: none;">
<div style="font-size: 0.75rem; color: #94a3b8; margin-bottom: 6px; font-weight: 600;">Candidate Live Notepad Notes (Auto-Synced):</div>
<pre id="modal-notes-display" style="background: #050811; border: 1px solid var(--card-border); border-radius: 10px; padding: 16px; font-family: monospace; font-size: 0.85rem; color: #e2e8f0; min-height: 250px; max-height: 350px; overflow-y: auto; white-space: pre-wrap;"></pre>
@ -577,21 +595,23 @@
</div>
</div>
<!-- Right Column: Proctoring Audit & 2-Way Real-Time Chat -->
<!-- Right Column: Proctoring Audit & Silent Warning Form -->
<div>
<span style="font-size: 0.8rem; font-weight: 600; color: white;">Proctoring Audit:</span>
<div id="modal-logs-display" style="background: rgba(0,0,0,0.4); border: 1px solid var(--card-border); border-radius: 10px; padding: 10px; max-height: 140px; overflow-y: auto; margin-bottom: 12px; font-size: 0.7rem; color: var(--text-muted);">
<div id="modal-logs-display" style="background: rgba(0,0,0,0.4); border: 1px solid var(--card-border); border-radius: 10px; padding: 10px; max-height: 200px; overflow-y: auto; margin-bottom: 14px; font-size: 0.7rem; color: var(--text-muted);">
No violations recorded.
</div>
<!-- 2-Way Real-Time Chat Container with Candidate -->
<!-- 2-Way Real-Time Chat Box in Review Modal -->
<div style="background: rgba(15,23,42,0.8); border: 1px solid var(--card-border); border-radius: 12px; padding: 12px;">
<div style="font-size: 0.75rem; color: #38bdf8; font-weight: 700; margin-bottom: 6px;">💬 2-Way Live Chat with Candidate:</div>
<div id="interviewer-chat-history" style="background: #050811; border: 1px solid var(--card-border); border-radius: 10px; padding: 10px; height: 150px; overflow-y: auto; font-size: 0.75rem; margin-bottom: 8px;">
<div style="color: #94a3b8; font-style: italic; text-align: center; padding-top: 50px;">No messages sent yet.</div>
<div style="font-size: 0.75rem; color: white; font-weight: 700; margin-bottom: 6px; display: flex; align-items: center; gap: 6px;">
💬 2-Way Live Chat History:
</div>
<div id="interviewer-chat-history" style="background: #050811; border: 1px solid var(--card-border); border-radius: 8px; padding: 8px; height: 130px; overflow-y: auto; font-size: 0.7rem; margin-bottom: 8px;">
<div style="color: #94a3b8; font-style: italic; text-align: center; padding-top: 40px;">No chat history. Send a message below.</div>
</div>
<div style="display: flex; gap: 6px;">
<input type="text" id="warning-input" placeholder="Type message to candidate and press Enter..." class="form-input" style="font-size: 0.75rem; flex: 1;" onkeypress="if(event.key==='Enter') sendSilentWarning()">
<input type="text" id="warning-input" placeholder="Type message to candidate..." onkeydown="if(event.key==='Enter') sendSilentWarning()" class="form-input" style="font-size: 0.75rem; background: #0b0f19; padding: 6px 10px; flex: 1;">
<button onclick="sendSilentWarning()" class="btn-nav" style="background: #6366f1; color: white; border: none; font-size: 0.75rem; font-weight: 600; padding: 6px 12px; cursor: pointer;">
Send
</button>
@ -601,126 +621,165 @@
</div>
</div>
<div style="display: flex; justify-content: flex-end; margin-top: 16px;">
<div style="display: flex; justify-content: flex-end;">
<button type="button" onclick="closeReviewModal()" class="btn-nav">Close</button>
</div>
</div>
</div>
<!-- SOS CHEATING ALERT MODAL (FOR INTERVIEWER ONLY) -->
<div id="sos-modal" class="admin-modal">
<div class="admin-modal-content" style="max-width: 500px; border: 2px solid #ef4444; background: #1a0505;">
<div style="text-align: center; margin-bottom: 16px;">
<div style="font-size: 3rem; margin-bottom: 6px;">🚨</div>
<h3 style="font-family: 'Outfit', sans-serif; font-size: 1.4rem; font-weight: 800; color: #ef4444;">SUSPICIOUS CHEATING DETECTED</h3>
<div style="font-size: 0.8rem; color: #fca5a5; margin-top: 4px;">Candidate proctoring engine has flagged unusual activities.</div>
<div id="sos-modal" class="admin-modal" style="z-index: 9999;">
<div class="admin-modal-content" style="max-width: 520px; border: 2px solid #ef4444; background: #0f172a; box-shadow: 0 0 40px rgba(239,68,68,0.8);">
<div style="display: flex; align-items: center; gap: 12px; border-bottom: 1px solid rgba(239,68,68,0.3); padding-bottom: 12px; margin-bottom: 14px;">
<div style="font-size: 2.2rem;">🚨</div>
<div>
<h3 style="color: #fca5a5; font-size: 1.25rem; font-weight: 800; margin: 0;">SOS CHEATING DETECTED</h3>
<div style="font-size: 0.72rem; color: #94a3b8;">Proctoring engine flagged candidate cheating activity!</div>
</div>
</div>
<div style="background: rgba(239,68,68,0.15); border: 1px solid rgba(239,68,68,0.3); border-radius: 12px; padding: 14px; margin-bottom: 20px;">
<div style="font-weight: 700; color: white; font-size: 0.85rem; margin-bottom: 6px;">Flagged Integrity Violations:</div>
<div id="sos-violation-list" style="font-size: 0.8rem; color: #fca5a5; line-height: 1.5;"></div>
<div id="sos-violation-list" style="background: rgba(239,68,68,0.1); border: 1px solid rgba(239,68,68,0.3); border-radius: 12px; padding: 14px; font-size: 0.8rem; color: #fca5a5; margin-bottom: 18px; max-height: 180px; overflow-y: auto;">
Violation details loading...
</div>
<div style="display: flex; gap: 10px; justify-content: center;">
<button onclick="stopSosAlarm()" class="btn-nav" style="background: #ef4444; color: white; border: none; font-weight: 700; padding: 10px 20px;">
🔕 Acknowledge & Stop Alarm
<div style="display: flex; gap: 10px; justify-content: flex-end;">
<button onclick="stopSosAlarm()" class="btn-nav" style="background: #ef4444; color: white; border: none; font-weight: 800; padding: 8px 16px; cursor: pointer;">
🔕 Acknowledge & Stop SOS Alarm
</button>
</div>
</div>
</div>
<style>
@keyframes sosPulse {
0% { transform: scale(1); box-shadow: 0 0 10px rgba(239,68,68,0.5); }
100% { transform: scale(1.05); box-shadow: 0 0 25px rgba(239,68,68,1); }
}
</style>
<script>
let currentInterviewId = null;
let pollTimer = null;
let activeViolations = [];
let acknowledgedViolationCount = 0;
let sosDismissed = false;
let autoSosEnabled = true;
let interviewerLocalStream = null;
let interviewerPeer = null;
let screenReceiverPeer = null;
let interviewerLocalStream = null;
let activeCall = null;
let isInterviewerMicOn = true;
let isInterviewerCamOn = true;
let screenReceiverPeer = null;
let candVidZoomLevel = 1.0;
let screenZoomLevel = 1.0;
function toggleAutoSosTrigger() {
autoSosEnabled = !autoSosEnabled;
const btn = document.getElementById('toggle-auto-sos-btn');
if (btn) {
btn.innerText = autoSosEnabled ? '🔔 SOS Auto-Trigger: ON' : '🔕 SOS Auto-Trigger: OFF';
btn.style.background = autoSosEnabled ? 'rgba(239,68,68,0.2)' : 'rgba(71,85,105,0.4)';
btn.style.borderColor = autoSosEnabled ? '#ef4444' : '#64748b';
btn.style.color = autoSosEnabled ? '#fca5a5' : '#94a3b8';
function getInitials(name) {
if (!name) return 'CD';
let parts = name.trim().split(/\s+/);
if (parts.length >= 2) {
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
}
let str = name.trim();
if (str.length >= 2) {
return (str[0] + str[str.length - 1]).toUpperCase();
}
return str.toUpperCase();
}
function zoomScreen(delta) {
screenZoomLevel = Math.max(0.5, Math.min(3.0, screenZoomLevel + delta));
const screenVid = document.getElementById('interviewer-screen-video');
const badge = document.getElementById('zoom-level-badge');
if (screenVid) {
screenVid.style.transform = `scale(${screenZoomLevel})`;
}
if (badge) {
badge.innerText = `(${Math.round(screenZoomLevel * 100)}%)`;
}
}
function openReviewModal(id, candName, candUid) {
function resetZoomScreen() {
screenZoomLevel = 1.0;
zoomScreen(0);
}
function toggleFullscreenScreen() {
const wrapper = document.getElementById('screen-wrapper');
if (!wrapper) return;
if (!document.fullscreenElement) {
wrapper.requestFullscreen().catch(err => console.log(err));
} else {
document.exitFullscreen();
}
}
let activeViolations = [];
let sosPollInterval = null;
let acknowledgedViolationCount = 0;
let sosDismissed = false;
let sosAutoTriggerDisabled = false;
function toggleSosAutoTrigger() {
sosAutoTriggerDisabled = !sosAutoTriggerDisabled;
const btn = document.getElementById('sos-toggle-btn');
if (sosAutoTriggerDisabled) {
btn.innerText = '🔕 SOS Auto-Trigger: OFF';
btn.style.background = 'rgba(239,68,68,0.15)';
btn.style.color = '#fca5a5';
btn.style.borderColor = '#ef4444';
} else {
btn.innerText = '🔔 SOS Auto-Trigger: ON';
btn.style.background = 'rgba(99,102,241,0.15)';
btn.style.color = '#818cf8';
btn.style.borderColor = '#6366f1';
}
}
function openReportPage() {
if (!currentInterviewId) return;
const reportUrl = `{{ route('interview.report', ':id') }}`.replace(':id', currentInterviewId);
window.open(reportUrl, '_blank');
}
function switchIdeTab(tab) {
document.getElementById('tab-content-code').style.display = (tab === 'code') ? 'block' : 'none';
document.getElementById('tab-content-notes').style.display = (tab === 'notes') ? 'block' : 'none';
document.getElementById('tab-content-drawing').style.display = (tab === 'drawing') ? 'block' : 'none';
document.getElementById('tab-btn-code').style.background = (tab === 'code') ? '#6366f1' : 'rgba(255,255,255,0.05)';
document.getElementById('tab-btn-code').style.color = (tab === 'code') ? 'white' : '#94a3b8';
document.getElementById('tab-btn-notes').style.background = (tab === 'notes') ? '#6366f1' : 'rgba(255,255,255,0.05)';
document.getElementById('tab-btn-notes').style.color = (tab === 'notes') ? 'white' : '#94a3b8';
document.getElementById('tab-btn-drawing').style.background = (tab === 'drawing') ? '#06b6d4' : 'rgba(255,255,255,0.05)';
document.getElementById('tab-btn-drawing').style.color = (tab === 'drawing') ? 'white' : '#94a3b8';
}
function openReviewModal(id, name, uid) {
currentInterviewId = id;
acknowledgedViolationCount = 0;
sosDismissed = false;
autoSosEnabled = true;
const autoBtn = document.getElementById('toggle-auto-sos-btn');
if (autoBtn) {
autoBtn.innerText = '🔔 SOS Auto-Trigger: ON';
autoBtn.style.background = 'rgba(239,68,68,0.2)';
autoBtn.style.borderColor = '#ef4444';
autoBtn.style.color = '#fca5a5';
}
document.getElementById('modal-cand-name').innerText = name;
document.getElementById('modal-cand-uid').innerText = 'Unique Submission ID: ' + uid;
document.getElementById('modal-cand-name').innerText = 'Reviewing Session: ' + candName;
document.getElementById('modal-cand-uid').innerText = 'Submission Unique ID: ' + candUid;
const candInitials = getInitials(name);
const avatarCircle = document.getElementById('modal-cand-avatar-circle');
const avatarName = document.getElementById('cand-avatar-name');
if (avatarCircle) avatarCircle.innerText = candInitials;
if (avatarName) avatarName.innerText = name;
const pdfBtn = document.getElementById('btn-modal-pdf-report');
if (pdfBtn) {
pdfBtn.href = `{{ route('interview.report', ':id') }}`.replace(':id', id);
}
const words = candName.trim().split(/\s+/);
let initials = 'CD';
if (words.length >= 2) {
initials = (words[0][0] + words[words.length - 1][0]).toUpperCase();
} else if (candName.length > 0) {
initials = candName.substr(0, 2).toUpperCase();
}
document.getElementById('modal-cand-avatar-circle').innerText = initials;
document.getElementById('cand-avatar-name').innerText = candName + ' (Camera)';
switchInspectorTab('code');
document.getElementById('review-modal').classList.add('active');
resetZoomScreen();
initInterviewerPeer();
switchIdeTab('code');
pollReviewData();
if (pollTimer) clearInterval(pollTimer);
pollTimer = setInterval(pollReviewData, 1500);
}
function closeReviewModal() {
document.getElementById('review-modal').classList.remove('active');
if (pollTimer) clearInterval(pollTimer);
leaveInterviewerCall();
}
function switchInspectorTab(tab) {
document.getElementById('tab-content-code').style.display = tab === 'code' ? 'block' : 'none';
document.getElementById('tab-content-notes').style.display = tab === 'notes' ? 'block' : 'none';
document.getElementById('tab-content-drawing').style.display = tab === 'drawing' ? 'block' : 'none';
document.getElementById('tab-btn-code').style.background = tab === 'code' ? '#6366f1' : 'rgba(255,255,255,0.05)';
document.getElementById('tab-btn-notes').style.background = tab === 'notes' ? '#6366f1' : 'rgba(255,255,255,0.05)';
document.getElementById('tab-btn-drawing').style.background = tab === 'drawing' ? '#6366f1' : 'rgba(255,255,255,0.05)';
clearInterval(sosPollInterval);
sosPollInterval = setInterval(pollReviewData, 2000);
}
function pollReviewData() {
if (!currentInterviewId) return;
fetch(`{{ route('interview.poll', ':id') }}`.replace(':id', currentInterviewId))
.then(r => r.json())
.then(data => {
document.getElementById('modal-lang-badge').innerText = (data.submitted_language || 'UNKNOWN').toUpperCase();
document.getElementById('modal-code-lang').innerText = (data.submitted_language || 'UNKNOWN').toUpperCase();
document.getElementById('modal-code-display').innerText = data.submitted_code || '// Candidate has not typed any code yet.';
document.getElementById('modal-output-display').innerText = data.code_output || 'No execution output.';
document.getElementById('modal-notes-display').innerText = data.candidate_notes || 'No candidate notes written yet.';
@ -736,28 +795,25 @@ function pollReviewData() {
noDrawing.style.display = 'block';
}
// Render 2-Way Chat History
const chatHistory = document.getElementById('interviewer-chat-history');
if (chatHistory && data.warnings) {
if (chatHistory && data.warnings && data.warnings.length > 0) {
let chatHtml = '';
if (data.warnings.length > 0) {
data.warnings.forEach(w => {
const isInterviewer = w.sent_by !== null;
const senderName = isInterviewer ? (w.sender ? w.sender.name : 'You (Interviewer)') : 'Candidate';
const badgeColor = isInterviewer ? '#6366f1' : '#38bdf8';
const bgColor = isInterviewer ? 'rgba(99,102,241,0.1)' : 'rgba(56,189,248,0.1)';
data.warnings.forEach(w => {
const isInterviewer = w.sent_by !== null;
const senderName = isInterviewer ? (w.sender ? w.sender.name : 'You (Interviewer)') : (data.candidate_name || 'Candidate');
const badgeColor = isInterviewer ? '#6366f1' : '#38bdf8';
const bgColor = isInterviewer ? 'rgba(99,102,241,0.1)' : 'rgba(56,189,248,0.1)';
chatHtml += `<div style="margin-bottom:8px; background:${bgColor}; padding:8px 10px; border-radius:8px; border-left:3px solid ${badgeColor};">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:4px;">
<strong style="color:${badgeColor}; font-size:0.75rem;">${senderName}</strong>
<span style="font-size:0.65rem; color:#94a3b8;">${new Date(w.created_at || Date.now()).toLocaleTimeString()}</span>
</div>
<div style="color: #ffffff !important; font-size:0.85rem; font-weight: 600; line-height:1.4; word-break: break-word;">${w.message}</div>
</div>`;
});
chatHistory.innerHTML = chatHtml;
chatHistory.scrollTop = chatHistory.scrollHeight;
}
chatHtml += `<div style="margin-bottom:8px; background:${bgColor}; padding:8px 10px; border-radius:8px; border-left:3px solid ${badgeColor};">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:4px;">
<strong style="color:${badgeColor}; font-size:0.75rem;">${senderName}</strong>
<span style="font-size:0.65rem; color:#94a3b8;">${new Date(w.created_at || Date.now()).toLocaleTimeString()}</span>
</div>
<div style="color: #ffffff !important; font-size:0.85rem; font-weight: 500; line-height:1.4;">${w.message}</div>
</div>`;
});
chatHistory.innerHTML = chatHtml;
chatHistory.scrollTop = chatHistory.scrollHeight;
}
// React to Global Call Status
@ -809,8 +865,8 @@ function pollReviewData() {
if (totalViolations > 0) {
sosBtn.style.display = 'inline-block';
// AUTOMATIC SOS TRIGGER & RE-ARMING (IF AUTO-SOS IS ENABLED)
if (autoSosEnabled && totalViolations > acknowledgedViolationCount) {
// AUTOMATIC SOS TRIGGER & RE-ARMING ON NEW CHEATING EVENTS (IF NOT MUTED BY PANEL)
if (totalViolations > acknowledgedViolationCount && !sosAutoTriggerDisabled) {
sosDismissed = false;
sosBtn.style.animation = 'sosPulse 1.2s infinite alternate';
sosBtn.innerText = '🚨 SOS CHEATING DETECTED!';
@ -827,8 +883,10 @@ function openSosModal() {
let html = '';
if (activeViolations.length > 0) {
activeViolations.forEach(v => {
let desc = v.details || v.type;
html += `• <strong>${v.type.toUpperCase()}</strong>: ${desc}<br>`;
html += `<div style="margin-bottom: 8px;">
<strong>🚨 ${v.type.toUpperCase()}:</strong> ${v.details || 'Suspicious candidate behavior detected.'}
<div style="font-size: 0.65rem; color: #94a3b8;">Timestamp: ${new Date(v.timestamp).toLocaleTimeString()}</div>
</div>`;
});
} else {
html = '<div>Potential proctoring alert flagged by engine.</div>';
@ -850,27 +908,6 @@ function stopSosAlarm() {
}
}
function sendSilentWarning() {
if (!currentInterviewId) return;
const input = document.getElementById('warning-input');
const msg = input.value.trim();
if (!msg) return;
fetch(`{{ route('interview.warning', ':id') }}`.replace(':id', currentInterviewId), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}'
},
body: JSON.stringify({ message: msg })
})
.then(r => r.json())
.then(res => {
input.value = '';
pollReviewData();
});
}
function interviewerLogoutAction() {
if (typeof Swal !== 'undefined') {
Swal.fire({
@ -1000,6 +1037,7 @@ function initInterviewerPeer() {
});
}
let candVidZoomLevel = 1.0;
function zoomCandVideo(delta) {
if (delta === 0) candVidZoomLevel = 1.0;
else candVidZoomLevel = Math.max(0.5, Math.min(3.0, candVidZoomLevel + delta));
@ -1013,35 +1051,6 @@ function zoomCandVideo(delta) {
if (badge) badge.innerText = `(${Math.round(candVidZoomLevel * 100)}%)`;
}
function zoomScreen(delta) {
screenZoomLevel = Math.max(0.5, Math.min(3.0, screenZoomLevel + delta));
applyScreenZoom();
}
function resetZoomScreen() {
screenZoomLevel = 1.0;
applyScreenZoom();
}
function applyScreenZoom() {
const vid = document.getElementById('interviewer-screen-video');
if (vid) {
vid.style.transform = `scale(${screenZoomLevel})`;
}
const badge = document.getElementById('zoom-level-badge');
if (badge) badge.innerText = `(${Math.round(screenZoomLevel * 100)}%)`;
}
function toggleFullscreenScreen() {
const elem = document.getElementById('screen-wrapper');
if (!document.fullscreenElement) {
if (elem.requestFullscreen) elem.requestFullscreen();
else if (elem.webkitRequestFullscreen) elem.webkitRequestFullscreen();
} else {
if (document.exitFullscreen) document.exitFullscreen();
}
}
let interviewerPeerConnection = null;
let interviewerSigChannel = null;
@ -1204,7 +1213,6 @@ function startInterviewerCall() {
});
};
// Non-blocking camera/mic permissions fallback
navigator.mediaDevices.getUserMedia({ video: true, audio: true })
.then(handleStream)
.catch(() => {
@ -1213,9 +1221,7 @@ function startInterviewerCall() {
.catch(() => {
navigator.mediaDevices.getUserMedia({ video: true, audio: false })
.then(handleStream)
.catch(() => {
handleStream(new MediaStream());
});
.catch(err => console.log('WebRTC Stream init:', err));
});
});
}
@ -1310,7 +1316,6 @@ function toggleInterviewerCam() {
.then(stream => {
interviewerLocalStream = stream;
if (selfVid) {
selfVid.muted = true;
selfVid.srcObject = stream;
selfVid.play().catch(e => console.log(e));
}
@ -1319,8 +1324,63 @@ function toggleInterviewerCam() {
.catch(err => console.log('Interviewer cam preview:', err));
return;
}
if (selfPlaceholder) selfPlaceholder.style.display = isInterviewerCamOn ? 'none' : 'flex';
}
function endInterviewerCall() {
if (interviewerLocalStream) {
interviewerLocalStream.getTracks().forEach(t => t.stop());
interviewerLocalStream = null;
}
if (activeCall) {
activeCall.close();
activeCall = null;
}
document.getElementById('cand-video-placeholder').style.display = 'flex';
document.getElementById('screen-video-placeholder').style.display = 'flex';
document.getElementById('self-video-placeholder').style.display = 'flex';
document.getElementById('btn-start-call').innerText = '📞 Start 2-Way Call';
document.getElementById('btn-start-call').style.background = '#10b981';
document.getElementById('btn-start-call').onclick = startInterviewerCall;
document.getElementById('btn-toggle-interviewer-mic').style.display = 'none';
document.getElementById('btn-toggle-interviewer-cam').style.display = 'none';
document.getElementById('interviewer-call-status').innerText = 'DISCONNECTED';
document.getElementById('interviewer-call-status').style.background = 'rgba(99,102,241,0.2)';
document.getElementById('interviewer-call-status').style.color = '#818cf8';
}
function closeReviewModal() {
clearInterval(sosPollInterval);
endInterviewerCall();
document.getElementById('review-modal').classList.remove('active');
}
function sendSilentWarning() {
const input = document.getElementById('warning-input');
const msg = input.value.trim();
if (!msg || !currentInterviewId) return;
input.value = '';
fetch(`{{ route('interview.warning', ':id') }}`.replace(':id', currentInterviewId), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}'
},
body: JSON.stringify({ message: msg })
})
.then(r => r.json())
.then(res => {
if (res.success) {
pollReviewData();
}
});
}
</script>
</body>
</html>