sls/resources/views/components/audit-log-item.blade.php

128 lines
8.2 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);
$parts = [];
if ($reasonsStr) $parts[] = $reasonsStr;
if (isset($detailsObj['yaw']) && is_numeric($detailsObj['yaw'])) $parts[] = 'Yaw: ' . ($detailsObj['yaw'] > 0 ? '+' : '') . number_format((float)$detailsObj['yaw'], 2);
if (isset($detailsObj['pitch']) && is_numeric($detailsObj['pitch'])) $parts[] = 'Pitch: ' . ($detailsObj['pitch'] > 0 ? '+' : '') . number_format((float)$detailsObj['pitch'], 2);
if (isset($detailsObj['gaze_horizontal']) && is_numeric($detailsObj['gaze_horizontal'])) $parts[] = 'Gaze H: ' . number_format((float)$detailsObj['gaze_horizontal'], 2);
if (isset($detailsObj['gaze_vertical']) && is_numeric($detailsObj['gaze_vertical'])) $parts[] = 'Gaze V: ' . number_format((float)$detailsObj['gaze_vertical'], 2);
$prefix = ($type === 'prolonged_looking_away') ? 'Looking away continuously (>10s) — ' : '';
$humanDetails = $prefix . implode(' | ', $parts);
} 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'] ?? 25000) / 1000);
$parts = ["Fixated on {$cornerLabel} for {$dur}s"];
if (isset($detailsObj['yaw']) && is_numeric($detailsObj['yaw'])) $parts[] = 'Yaw: ' . ($detailsObj['yaw'] > 0 ? '+' : '') . number_format((float)$detailsObj['yaw'], 2);
if (isset($detailsObj['pitch']) && is_numeric($detailsObj['pitch'])) $parts[] = 'Pitch: ' . ($detailsObj['pitch'] > 0 ? '+' : '') . number_format((float)$detailsObj['pitch'], 2);
if (isset($detailsObj['gaze_horizontal']) && is_numeric($detailsObj['gaze_horizontal'])) $parts[] = 'Gaze H: ' . number_format((float)$detailsObj['gaze_horizontal'], 2);
if (isset($detailsObj['gaze_vertical']) && is_numeric($detailsObj['gaze_vertical'])) $parts[] = 'Gaze V: ' . number_format((float)$detailsObj['gaze_vertical'], 2);
$humanDetails = implode(' | ', $parts);
} else {
$humanDetails = 'Candidate fixated on screen corner for prolonged period (25+ seconds)';
}
} 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> &mdash; {{ $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']) ? ' &bull; Detected: <strong>' . e($aiVerdict['ai_tool_detected']) . '</strong>' : '';
@endphp
<div class="mt-1 text-[11px] text-red-300"><b>AI inspector &middot; {{ $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>