116 lines
6.7 KiB
PHP
116 lines
6.7 KiB
PHP
@props([
|
|
'type' => 'violation',
|
|
'details' => '',
|
|
'timestamp' => null,
|
|
'screenshotUrl' => null,
|
|
'aiVerdict' => null,
|
|
])
|
|
|
|
@php
|
|
$icoClass = 'amber';
|
|
$faIcon = 'fa-solid fa-window-restore';
|
|
$isFlag = false;
|
|
|
|
if (in_array($type, ['focus_lost', 'tab_switch'])) {
|
|
$icoClass = 'amber'; $faIcon = 'fa-solid fa-window-restore';
|
|
} elseif (in_array($type, ['gaze_anomaly', 'gaze_fixed_staring'])) {
|
|
$icoClass = 'amber'; $faIcon = 'fa-solid fa-eye';
|
|
} elseif ($type === 'looking_away') {
|
|
$icoClass = 'amber'; $faIcon = 'fa-solid fa-eye-slash';
|
|
} elseif ($type === 'face_missing') {
|
|
$icoClass = 'red'; $faIcon = 'fa-solid fa-user-slash'; $isFlag = true;
|
|
} elseif ($type === 'multiple_faces') {
|
|
$icoClass = 'red'; $faIcon = 'fa-solid fa-users-viewfinder'; $isFlag = true;
|
|
} elseif ($type === 'prolonged_looking_away') {
|
|
$icoClass = 'red'; $faIcon = 'fa-solid fa-clock-rotate-left'; $isFlag = true;
|
|
} elseif ($type === 'phone_detected') {
|
|
$icoClass = 'red'; $faIcon = 'fa-solid fa-mobile-screen-button'; $isFlag = true;
|
|
} elseif ($type === 'prolonged_corner_gaze') {
|
|
$icoClass = 'amber'; $faIcon = 'fa-solid fa-arrows-to-eye'; $isFlag = true;
|
|
} elseif ($type === 'talking_secondary_person') {
|
|
$icoClass = 'red'; $faIcon = 'fa-solid fa-user-group'; $isFlag = true;
|
|
} elseif (in_array($type, ['gaze_lower_device', 'reading_external_device'])) {
|
|
$icoClass = 'red'; $faIcon = 'fa-solid fa-mobile-screen'; $isFlag = true;
|
|
} elseif (in_array($type, ['question_repeat_lower', 'question_repetition'])) {
|
|
$icoClass = 'red'; $faIcon = 'fa-solid fa-comments'; $isFlag = true;
|
|
} elseif ($type === 'talking_on_phone') {
|
|
$icoClass = 'red'; $faIcon = 'fa-solid fa-phone'; $isFlag = true;
|
|
} elseif ($type === 'external_ai_detected') {
|
|
$icoClass = 'red'; $faIcon = 'fa-solid fa-robot'; $isFlag = true;
|
|
} elseif ($type === 'paste_event') {
|
|
$icoClass = 'red'; $faIcon = 'fa-solid fa-paste'; $isFlag = true;
|
|
} elseif ($type === 'copy_event') {
|
|
$icoClass = 'amber'; $faIcon = 'fa-solid fa-copy';
|
|
}
|
|
|
|
$titleCap = ucfirst(str_replace('_', ' ', $type));
|
|
$timeStr = $timestamp ? \Carbon\Carbon::parse($timestamp)->format('h:i:s a') : now()->format('h:i:s a');
|
|
|
|
// Human readable details parsing
|
|
$detailsObj = is_array($details) ? $details : (is_string($details) && str_starts_with(trim($details), '{') ? json_decode($details, true) : null);
|
|
$humanDetails = '';
|
|
|
|
if ($type === 'multiple_faces') {
|
|
$count = (is_array($detailsObj) && isset($detailsObj['face_count'])) ? $detailsObj['face_count'] : 2;
|
|
$humanDetails = "Multiple faces detected in camera frame ({$count} faces)";
|
|
} elseif ($type === 'face_missing') {
|
|
$humanDetails = 'Candidate face is not visible in camera frame';
|
|
} elseif (in_array($type, ['looking_away', 'prolonged_looking_away'])) {
|
|
if (is_array($detailsObj) && (isset($detailsObj['reasons']) || isset($detailsObj['yaw']))) {
|
|
$reasonLabels = [
|
|
'head_yaw' => 'Head Yaw',
|
|
'head_up' => 'Head Pitch Up',
|
|
'head_down' => 'Head Pitch Down',
|
|
'eye_gaze_left' => 'Eye Gaze Left',
|
|
'eye_gaze_right' => 'Eye Gaze Right',
|
|
'eye_gaze_down' => 'Eye Gaze Down'
|
|
];
|
|
$reasonsArr = isset($detailsObj['reasons']) && is_array($detailsObj['reasons']) ? array_map(fn($r) => $reasonLabels[$r] ?? str_replace('_', ' ', $r), $detailsObj['reasons']) : [];
|
|
$reasonsStr = implode(', ', $reasonsArr);
|
|
|
|
$prefix = ($type === 'prolonged_looking_away') ? 'Looking away continuously (>10s)' : 'Looking away from camera';
|
|
$humanDetails = $reasonsStr ? "{$prefix} ({$reasonsStr})" : $prefix;
|
|
} else {
|
|
$humanDetails = is_string($details) && !str_starts_with(trim($details), '{') ? $details : ($type === 'prolonged_looking_away' ? 'Candidate prolonged looking away' : 'Candidate looking away from camera');
|
|
}
|
|
} elseif ($type === 'phone_detected') {
|
|
$label = (is_array($detailsObj) && isset($detailsObj['label'])) ? $detailsObj['label'] : 'cell phone';
|
|
$humanDetails = "Possible mobile phone detected in camera frame ({$label})";
|
|
} elseif ($type === 'prolonged_corner_gaze') {
|
|
if (is_array($detailsObj) && (isset($detailsObj['corner_label']) || isset($detailsObj['corner']))) {
|
|
$cornerLabel = $detailsObj['corner_label'] ?? str_replace('_', ' ', $detailsObj['corner'] ?? 'screen corner');
|
|
$dur = $detailsObj['duration_s'] ?? round(($detailsObj['duration_ms'] ?? 18000) / 1000);
|
|
$humanDetails = "Fixated on {$cornerLabel} for {$dur}s";
|
|
} else {
|
|
$humanDetails = 'Candidate fixated on screen corner for prolonged period';
|
|
}
|
|
} elseif ($type === 'talking_secondary_person') {
|
|
$humanDetails = is_string($details) && !str_starts_with(trim($details), '{') ? $details : 'Candidate detected speaking out loud to a secondary person';
|
|
} else {
|
|
$humanDetails = is_string($details) ? $details : (is_array($details) ? json_encode($details) : (string)$details);
|
|
}
|
|
@endphp
|
|
|
|
<div {{ $attributes->merge(['class' => 'log-item flex gap-2.5 p-3 border-b border-[#1b2233] last:border-b-0 ' . ($isFlag ? 'bg-[rgba(239,74,95,0.1)] flag' : '')]) }}>
|
|
<div class="log-ico w-6 h-6 rounded-md flex-shrink-0 flex items-center justify-center text-[11px] mt-0.5 {{ $icoClass === 'amber' ? 'bg-[rgba(240,169,57,0.12)] text-[#f0a939]' : 'bg-[rgba(239,74,95,0.1)] text-[#ef4a5f]' }}">
|
|
<i class="{{ $faIcon }}"></i>
|
|
</div>
|
|
<div>
|
|
<div class="log-text text-xs leading-relaxed text-slate-300">
|
|
<b class="text-white {{ $isFlag ? '!text-[#ef4a5f]' : '' }}">{{ $titleCap }}</b> — {{ $humanDetails }}
|
|
@if($screenshotUrl)
|
|
<a href="{{ $screenshotUrl }}" target="_blank" class="text-sky-400 underline ml-1 font-medium">[<i class="fa-solid fa-camera mr-0.5"></i> Screenshot]</a>
|
|
@endif
|
|
@if($aiVerdict)
|
|
@php
|
|
$confPct = round(($aiVerdict['confidence'] ?? 0.85) * 100);
|
|
$engineName = $aiVerdict['engine'] ?? 'AI Engine';
|
|
$toolName = !empty($aiVerdict['ai_tool_detected']) ? ' • Detected: <strong>' . e($aiVerdict['ai_tool_detected']) . '</strong>' : '';
|
|
@endphp
|
|
<div class="mt-1 text-[11px] text-red-300"><b>AI inspector · {{ $confPct }}% cheating confidence</b> ({!! $engineName . $toolName !!})</div>
|
|
@endif
|
|
</div>
|
|
<div class="log-time text-[10.5px] text-slate-500 mt-0.5">{{ $timeStr }}</div>
|
|
</div>
|
|
</div>
|