new_fixes #18
@ -279,9 +279,9 @@ public function logViolation(Request $request, $id)
|
|||||||
$type = $request->input('type'); // tab_switch, focus_lost, paste_event, gaze_anomaly, question_repetition, external_ai_detected
|
$type = $request->input('type'); // tab_switch, focus_lost, paste_event, gaze_anomaly, question_repetition, external_ai_detected
|
||||||
$details = $request->input('details', '');
|
$details = $request->input('details', '');
|
||||||
|
|
||||||
// Automatically run AI Speech/Transcript Inspection if violation is speech/question repetition
|
// Automatically run AI Speech/Transcript Inspection if violation is speech/question repetition/phone call
|
||||||
$aiVerdict = null;
|
$aiVerdict = null;
|
||||||
if (in_array($type, ['question_repetition', 'question_repeat_lower', 'talking_secondary_person'])) {
|
if (in_array($type, ['question_repetition', 'question_repeat_lower', 'talking_secondary_person', 'talking_on_phone', 'reading_external_device'])) {
|
||||||
$aiDetector = new \App\Services\AiCheatingDetectorService();
|
$aiDetector = new \App\Services\AiCheatingDetectorService();
|
||||||
$aiVerdict = $aiDetector->analyzeSpeechTranscript($details, $interview->candidate_notes ?? '');
|
$aiVerdict = $aiDetector->analyzeSpeechTranscript($details, $interview->candidate_notes ?? '');
|
||||||
}
|
}
|
||||||
@ -771,10 +771,19 @@ public function uploadTabScreenshot(Request $request, $id)
|
|||||||
$screenshots = $interview->tab_switch_screenshots ?? [];
|
$screenshots = $interview->tab_switch_screenshots ?? [];
|
||||||
$screenshots[] = $screenshotEntry;
|
$screenshots[] = $screenshotEntry;
|
||||||
|
|
||||||
$aiDetails = $aiVerdict['summary'] . ' (' . round(($aiVerdict['confidence'] ?? 0.85) * 100) . '% Confidence - ' . ($aiVerdict['engine'] ?? 'AI Engine') . ')';
|
$hasAiTool = !empty($aiVerdict['is_cheating']) && !empty($aiVerdict['ai_tool_detected']);
|
||||||
|
|
||||||
$logType = ($reason === 'call_start') ? 'call_start_screenshot' : (($aiVerdict['is_cheating'] && str_contains(strtolower($aiVerdict['ai_tool_detected'] ?? ''), 'parakeet')) ? 'external_ai_detected' : 'tab_switch');
|
if ($reason === 'call_start') {
|
||||||
$logMsg = ($reason === 'call_start') ? '📸 CALL STARTED SYSTEM SNAPSHOT: System screen snapshot captured automatically when call started.' : ('🤖 AI INSPECTOR VERDICT: ' . $aiDetails);
|
$logType = 'call_start_screenshot';
|
||||||
|
$logMsg = '📸 CALL STARTED SYSTEM SNAPSHOT: System screen snapshot captured automatically when call started.';
|
||||||
|
} elseif ($hasAiTool) {
|
||||||
|
$logType = 'external_ai_detected';
|
||||||
|
$toolName = $aiVerdict['ai_tool_detected'];
|
||||||
|
$logMsg = "🤖 candidate might use external ai ({$toolName}): " . $aiVerdict['summary'];
|
||||||
|
} else {
|
||||||
|
$logType = 'tab_switch';
|
||||||
|
$logMsg = '📸 Candidate switched browser tab or window: System screen screenshot captured for interviewer & admin review.';
|
||||||
|
}
|
||||||
|
|
||||||
$logs = $interview->proctor_logs ?? [];
|
$logs = $interview->proctor_logs ?? [];
|
||||||
$logs[] = [
|
$logs[] = [
|
||||||
|
|||||||
@ -41,7 +41,7 @@ public function analyzeScreenshot(string $imagePathOrBase64): array
|
|||||||
[
|
[
|
||||||
'parts' => [
|
'parts' => [
|
||||||
[
|
[
|
||||||
'text' => "You are an AI Anti-Cheat Proctoring Security Inspector for live technical interviews. Analyze this candidate desktop screen capture. Look specifically for external AI assistant overlays (Parakeet AI, Final Round AI, Otter.ai, ChatGPT, Claude, GitHub Copilot, WhatsApp, cheat sheets, or unauthorized secondary coding windows). Respond ONLY with valid JSON in this exact structure: {\"is_cheating\": boolean, \"confidence\": number (0.0 to 1.0), \"ai_tool_detected\": string or null, \"summary\": string}"
|
'text' => "You are an AI Anti-Cheat Proctoring Security Inspector for live technical interviews. Analyze this candidate desktop screen capture. Look specifically for external AI assistant overlays (Parakeet AI, Final Round AI, Otter.ai, ChatGPT, Claude, GitHub Copilot, WhatsApp, cheat sheets, or unauthorized secondary coding windows). If an external AI assistant or cheat tool is open/visible, set is_cheating to true, specify ai_tool_detected with the name of the tool, and set summary to 'candidate might use external ai: ' followed by details. If no external AI tool is open, set is_cheating to false, ai_tool_detected to null, and summary to 'Candidate desktop screenshot captured. No external AI detected.' Respond ONLY with valid JSON in this exact structure: {\"is_cheating\": boolean, \"confidence\": number (0.0 to 1.0), \"ai_tool_detected\": string or null, \"summary\": string}"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'inline_data' => [
|
'inline_data' => [
|
||||||
@ -64,11 +64,13 @@ public function analyzeScreenshot(string $imagePathOrBase64): array
|
|||||||
$data = json_decode($text, true);
|
$data = json_decode($text, true);
|
||||||
|
|
||||||
if (is_array($data)) {
|
if (is_array($data)) {
|
||||||
|
$isCheating = (bool) ($data['is_cheating'] ?? false);
|
||||||
|
$aiTool = $data['ai_tool_detected'] ?? null;
|
||||||
return [
|
return [
|
||||||
'is_cheating' => (bool) ($data['is_cheating'] ?? false),
|
'is_cheating' => $isCheating,
|
||||||
'confidence' => (float) ($data['confidence'] ?? 0.85),
|
'confidence' => (float) ($data['confidence'] ?? 0.85),
|
||||||
'ai_tool_detected' => $data['ai_tool_detected'] ?? null,
|
'ai_tool_detected' => $aiTool,
|
||||||
'summary' => $data['summary'] ?? 'AI Screen Inspection completed.',
|
'summary' => $data['summary'] ?? ($isCheating ? 'candidate might use external ai' : 'AI Screen Inspection completed.'),
|
||||||
'engine' => 'Gemini Vision AI'
|
'engine' => 'Gemini Vision AI'
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@ -78,13 +80,13 @@ public function analyzeScreenshot(string $imagePathOrBase64): array
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback Heuristic Inspection Engine (Local Rule-based AI Classifier)
|
// Fallback Inspection Engine (Do not falsely claim AI cheating unless explicitly detected)
|
||||||
return [
|
return [
|
||||||
'is_cheating' => true,
|
'is_cheating' => false,
|
||||||
'confidence' => 0.92,
|
'confidence' => 0.85,
|
||||||
'ai_tool_detected' => 'External Desktop / App Switch Detected',
|
'ai_tool_detected' => null,
|
||||||
'summary' => 'Candidate switched away from assessment window. Desktop screen snapshot captured for interviewer review.',
|
'summary' => 'Candidate desktop screenshot captured for interviewer & admin review. No external AI tool detected.',
|
||||||
'engine' => 'SingleLogin Anti-Cheat Heuristic AI'
|
'engine' => 'SingleLogin Anti-Cheat Inspection'
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -459,8 +459,8 @@
|
|||||||
const code = e.browserEvent ? (e.browserEvent.code || '') : '';
|
const code = e.browserEvent ? (e.browserEvent.code || '') : '';
|
||||||
|
|
||||||
if (isAlt || isMeta || (isCtrl && isShift) || code === 'F12') {
|
if (isAlt || isMeta || (isCtrl && isShift) || code === 'F12') {
|
||||||
logViolation('external_ai_detected', `Parakeet AI / External AI Hotkey Intercepted in Editor (${code || e.keyCode}): Candidate activated external AI shortcut`);
|
logViolation('external_ai_detected', `candidate might use external ai: Intercepted AI hotkey shortcut in editor (${code || e.keyCode})`);
|
||||||
captureAndUploadTabScreenshot();
|
captureAndUploadTabScreenshot('external_ai_detected');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -609,6 +609,8 @@ function getScreenShareVideoElement() {
|
|||||||
// REAL CANDIDATE SYSTEM / COMPUTER SCREENSHOT CAPTURE ENGINE
|
// REAL CANDIDATE SYSTEM / COMPUTER SCREENSHOT CAPTURE ENGINE
|
||||||
function captureAndUploadTabScreenshot(reason = 'tab_switch') {
|
function captureAndUploadTabScreenshot(reason = 'tab_switch') {
|
||||||
if (!isTabSwitchScreenshotEnabled) return;
|
if (!isTabSwitchScreenshotEnabled) return;
|
||||||
|
if (latestCallStatus !== 'active') return; // Screenshot ONLY when call is active
|
||||||
|
if (reason !== 'tab_switch' && reason !== 'call_start') return; // Screenshot ONLY when candidate switches tab
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const tempCanvas = document.createElement('canvas');
|
const tempCanvas = document.createElement('canvas');
|
||||||
@ -757,25 +759,25 @@ function uploadTabScreenshotBlob(imageData, reason = 'tab_switch') {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2. Window Focus Loss / External AI Overlay Application Switch Detection
|
// 2. Window Focus Loss / Assessment Window Blur Detection
|
||||||
window.addEventListener('blur', function () {
|
window.addEventListener('blur', function () {
|
||||||
if (isFocusSuppressed()) return;
|
if (isFocusSuppressed()) return;
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (now - lastTabSwitchTime > 1000) {
|
if (now - lastTabSwitchTime > 1000) {
|
||||||
lastTabSwitchTime = now;
|
lastTabSwitchTime = now;
|
||||||
logViolation('external_ai_detected', 'PARAKEET AI / EXTERNAL APP OVERLAY DETECTED: Assessment window lost focus (Candidate interacting with external application overlay)');
|
logViolation('tab_switch', 'Candidate assessment window lost focus');
|
||||||
captureAndUploadTabScreenshot();
|
captureAndUploadTabScreenshot('tab_switch');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Mouse Viewport Departure (Candidate moving cursor outside window to interact with Parakeet AI or secondary app overlay)
|
// Mouse Viewport Departure (Candidate moving cursor outside window)
|
||||||
document.addEventListener('mouseleave', function () {
|
document.addEventListener('mouseleave', function () {
|
||||||
if (isFocusSuppressed()) return;
|
if (isFocusSuppressed()) return;
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (now - lastTabSwitchTime > 2500) {
|
if (now - lastTabSwitchTime > 2500) {
|
||||||
lastTabSwitchTime = now;
|
lastTabSwitchTime = now;
|
||||||
logViolation('external_ai_detected', 'Candidate cursor departed assessment window viewport (suspected external Parakeet AI overlay interaction)');
|
logViolation('tab_switch', 'Candidate cursor departed assessment window viewport');
|
||||||
captureAndUploadTabScreenshot();
|
captureAndUploadTabScreenshot('tab_switch');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -810,8 +812,8 @@ function uploadTabScreenshotBlob(imageData, reason = 'tab_switch') {
|
|||||||
(key === 'f12' || code === 'f12' || key === 'f11' || code === 'f11');
|
(key === 'f12' || code === 'f12' || key === 'f11' || code === 'f11');
|
||||||
|
|
||||||
if (isAiHotkey) {
|
if (isAiHotkey) {
|
||||||
logViolation('external_ai_detected', `Parakeet AI / External AI Hotkey Intercepted (${e.code || e.key}): Candidate activated external AI assistant overlay shortcut`);
|
logViolation('external_ai_detected', `candidate might use external ai: Intercepted AI assistant shortcut hotkey (${e.code || e.key})`);
|
||||||
captureAndUploadTabScreenshot();
|
captureAndUploadTabScreenshot('external_ai_detected');
|
||||||
}
|
}
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
@ -823,8 +825,8 @@ function uploadTabScreenshotBlob(imageData, reason = 'tab_switch') {
|
|||||||
if (node.nodeType === 1) { // Element node
|
if (node.nodeType === 1) { // Element node
|
||||||
const nodeStr = ((node.id || '') + ' ' + (node.className || '') + ' ' + (node.getAttribute('src') || '')).toLowerCase();
|
const nodeStr = ((node.id || '') + ' ' + (node.className || '') + ' ' + (node.getAttribute('src') || '')).toLowerCase();
|
||||||
if (nodeStr.includes('parakeet') || nodeStr.includes('finalround') || nodeStr.includes('copilot') || nodeStr.includes('ai-assistant') || nodeStr.includes('otter')) {
|
if (nodeStr.includes('parakeet') || nodeStr.includes('finalround') || nodeStr.includes('copilot') || nodeStr.includes('ai-assistant') || nodeStr.includes('otter')) {
|
||||||
logViolation('external_ai_detected', 'Parakeet AI / Extension Overlay Element Injected into DOM: ' + node.tagName);
|
logViolation('external_ai_detected', 'candidate might use external ai: Injected AI extension overlay element detected in DOM (' + node.tagName + ')');
|
||||||
captureAndUploadTabScreenshot();
|
captureAndUploadTabScreenshot('external_ai_detected');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -836,7 +838,7 @@ function uploadTabScreenshotBlob(imageData, reason = 'tab_switch') {
|
|||||||
// 6. Picture-in-Picture / Floating Overlay Window Detector
|
// 6. Picture-in-Picture / Floating Overlay Window Detector
|
||||||
setInterval(function() {
|
setInterval(function() {
|
||||||
if (document.pictureInPictureElement) {
|
if (document.pictureInPictureElement) {
|
||||||
logViolation('external_ai_detected', 'External Picture-in-Picture Floating Window Active: Suspected AI assistant overlay window');
|
logViolation('external_ai_detected', 'candidate might use external ai: External Picture-in-Picture floating AI window active');
|
||||||
}
|
}
|
||||||
}, 6000);
|
}, 6000);
|
||||||
|
|
||||||
@ -897,13 +899,13 @@ function analyzeEyeGazeAndStaring() {
|
|||||||
|
|
||||||
// 2.4s initial alert
|
// 2.4s initial alert
|
||||||
if (gazeDownwardCounter === 6) {
|
if (gazeDownwardCounter === 6) {
|
||||||
logViolation('gaze_anomaly', 'Candidate lowered eye gaze toward screen bottom or desk');
|
logViolation('gaze_anomaly', 'Candidate lowered eye gaze toward screen bottom, desk, or secondary device');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 10.0s continuous lower screen stare alert (logs to proctor logs section in reviewer modal - SILENT FOR CANDIDATE)
|
// 10.0s continuous lower screen stare alert
|
||||||
if (gazeDownwardDurationMs >= 10000) {
|
if (gazeDownwardDurationMs >= 10000) {
|
||||||
gazeDownwardDurationMs = 0; // Reset timer for next 10s window if candidate continues looking down
|
gazeDownwardDurationMs = 0;
|
||||||
logViolation('gaze_lower_device', 'Candidate continuously looking at lower portion of screen for 10s (suspected reading from mobile device or cheat sheet)');
|
logViolation('reading_external_device', 'Candidate continuously looking down at lower screen or desk for 10s (suspected reading from mobile device, secondary screen, or cheat sheet)');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
gazeDownwardCounter = Math.max(0, gazeDownwardCounter - 1);
|
gazeDownwardCounter = Math.max(0, gazeDownwardCounter - 1);
|
||||||
@ -923,7 +925,7 @@ function analyzeEyeGazeAndStaring() {
|
|||||||
if (frameDelta >= 0.1 && frameDelta < 3.8) {
|
if (frameDelta >= 0.1 && frameDelta < 3.8) {
|
||||||
gazeFixedStaringCounter++;
|
gazeFixedStaringCounter++;
|
||||||
if (gazeFixedStaringCounter === 25) { // 10.0 seconds of continuous fixed stare
|
if (gazeFixedStaringCounter === 25) { // 10.0 seconds of continuous fixed stare
|
||||||
logViolation('gaze_fixed_staring', 'Candidate maintaining fixed unnatural gaze (possible secondary screen/AI assistant)');
|
logViolation('reading_external_device', 'Candidate maintaining fixed unnatural off-center gaze for 10s (suspected reading from external device or secondary monitor)');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
gazeFixedStaringCounter = Math.max(0, gazeFixedStaringCounter - 1);
|
gazeFixedStaringCounter = Math.max(0, gazeFixedStaringCounter - 1);
|
||||||
@ -972,11 +974,17 @@ function initQuestionRepeatDetection() {
|
|||||||
candidateSpeechHistory.push(transcript);
|
candidateSpeechHistory.push(transcript);
|
||||||
if (candidateSpeechHistory.length > 15) candidateSpeechHistory.shift();
|
if (candidateSpeechHistory.length > 15) candidateSpeechHistory.shift();
|
||||||
|
|
||||||
if (isReadingQuestionOnScreen || isRepeatedSpeech) {
|
// Phone call speech detection keywords
|
||||||
|
const phoneKeywords = ['hello', 'can you hear me', 'on phone', 'calling', 'call you back', 'hold on', 'on the line', 'speaker', 'phone', 'mobile', 'call me', 'speak later', 'hey call', 'on a call'];
|
||||||
|
const isPhoneTalk = phoneKeywords.some(kw => transcript.includes(kw));
|
||||||
|
|
||||||
|
if (isPhoneTalk) {
|
||||||
|
logViolation('talking_on_phone', `Candidate detected talking on phone / phone call during assessment: "${transcript}"`);
|
||||||
|
} else if (isReadingQuestionOnScreen || isRepeatedSpeech) {
|
||||||
if (gazeDownwardCounter > 3) {
|
if (gazeDownwardCounter > 3) {
|
||||||
logViolation('question_repeat_lower', `Candidate reading/repeating question ("${transcript}") while looking down at lower portion/mobile device`);
|
logViolation('reading_external_device', `Candidate detected reading from external device or mobile screen: "${transcript}"`);
|
||||||
} else {
|
} else {
|
||||||
logViolation('question_repetition', `Candidate reading/repeating question out loud: "${transcript}" (suspected Parakeet AI speech prompt)`);
|
logViolation('question_repetition', `Candidate detected reading / repeating question out loud: "${transcript}" (suspected AI prompt ingestion)`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Talking to someone else detection when speech is detected during non-call state
|
// Talking to someone else detection when speech is detected during non-call state
|
||||||
@ -1623,13 +1631,6 @@ function startCandidateCameraStream() {
|
|||||||
// Initialize candidate camera immediately on room load
|
// Initialize candidate camera immediately on room load
|
||||||
startCandidateCameraStream();
|
startCandidateCameraStream();
|
||||||
|
|
||||||
// Prompt candidate to activate Entire Screen Proctoring Shield upon joining
|
|
||||||
setTimeout(() => {
|
|
||||||
if (!screenShareStream) {
|
|
||||||
promptScreenShareRequired();
|
|
||||||
}
|
|
||||||
}, 1500);
|
|
||||||
|
|
||||||
|
|
||||||
function candidateLogoutAction() {
|
function candidateLogoutAction() {
|
||||||
if (typeof Swal !== 'undefined') {
|
if (typeof Swal !== 'undefined') {
|
||||||
@ -1803,8 +1804,8 @@ function initLiveOverlayDetector() {
|
|||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (now - lastTabSwitchTime > 3000) {
|
if (now - lastTabSwitchTime > 3000) {
|
||||||
lastTabSwitchTime = now;
|
lastTabSwitchTime = now;
|
||||||
logViolation('external_ai_detected', 'PARAKEET AI OVERLAY DETECTED: External desktop overlay active on screen!');
|
logViolation('tab_switch', 'Candidate window lost focus during active screen share');
|
||||||
captureAndUploadTabScreenshot();
|
captureAndUploadTabScreenshot('tab_switch');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch(e) {}
|
} catch(e) {}
|
||||||
@ -1930,8 +1931,7 @@ function startScreenShare() {
|
|||||||
btn.innerText = '🖥️ Share Screen with Interviewer';
|
btn.innerText = '🖥️ Share Screen with Interviewer';
|
||||||
btn.style.background = 'linear-gradient(135deg, #6366f1 0%, #0078d4 100%)';
|
btn.style.background = 'linear-gradient(135deg, #6366f1 0%, #0078d4 100%)';
|
||||||
}
|
}
|
||||||
logViolation('tab_switch', 'Candidate stopped screen sharing (proctoring screen share disabled)');
|
logViolation('tab_switch', 'Candidate stopped screen sharing');
|
||||||
promptScreenShareRequired('Screen sharing was stopped. You must share your Entire Screen to continue.');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
initLiveOverlayDetector();
|
initLiveOverlayDetector();
|
||||||
@ -1940,8 +1940,11 @@ function startScreenShare() {
|
|||||||
endNativePrompt();
|
endNativePrompt();
|
||||||
console.log('Screen sharing cancelled/denied:', err);
|
console.log('Screen sharing cancelled/denied:', err);
|
||||||
screenShareStream = null;
|
screenShareStream = null;
|
||||||
logViolation('tab_switch', 'Candidate denied or cancelled screen sharing permission prompt');
|
const btn = document.getElementById('share-screen-btn');
|
||||||
promptScreenShareRequired('Screen sharing permission was denied or cancelled.');
|
if (btn) {
|
||||||
|
btn.innerText = '🖥️ Share Screen with Interviewer';
|
||||||
|
btn.style.background = 'linear-gradient(135deg, #6366f1 0%, #0078d4 100%)';
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1506,12 +1506,13 @@ function pollReviewData() {
|
|||||||
let icon = '🔴';
|
let icon = '🔴';
|
||||||
if (l.type === 'focus_lost') icon = '🟡';
|
if (l.type === 'focus_lost') icon = '🟡';
|
||||||
if (l.type === 'gaze_anomaly' || l.type === 'gaze_fixed_staring') icon = '👁️';
|
if (l.type === 'gaze_anomaly' || l.type === 'gaze_fixed_staring') icon = '👁️';
|
||||||
if (l.type === 'gaze_lower_device') icon = '📱';
|
if (l.type === 'gaze_lower_device' || l.type === 'reading_external_device') icon = '📱';
|
||||||
if (l.type === 'question_repeat_lower' || l.type === 'question_repetition') icon = '🗣️';
|
if (l.type === 'question_repeat_lower' || l.type === 'question_repetition') icon = '🗣️';
|
||||||
|
if (l.type === 'talking_on_phone') icon = '📞';
|
||||||
if (l.type === 'external_ai_detected') icon = '🤖';
|
if (l.type === 'external_ai_detected') icon = '🤖';
|
||||||
if (l.type === 'tab_switch') icon = '📸';
|
if (l.type === 'tab_switch') icon = '📸';
|
||||||
|
|
||||||
if (['paste_event', 'tab_switch', 'focus_lost', 'gaze_anomaly', 'gaze_fixed_staring', 'gaze_lower_device', 'question_repeat_lower', 'question_repetition', 'external_ai_detected', 'talking_secondary_person'].includes(l.type)) {
|
if (['paste_event', 'tab_switch', 'focus_lost', 'gaze_anomaly', 'gaze_fixed_staring', 'gaze_lower_device', 'question_repeat_lower', 'question_repetition', 'external_ai_detected', 'talking_secondary_person', 'talking_on_phone', 'reading_external_device'].includes(l.type)) {
|
||||||
currentViolations.push(l);
|
currentViolations.push(l);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1550,8 +1551,22 @@ function pollReviewData() {
|
|||||||
if (totalViolations > acknowledgedViolationCount && !sosAutoTriggerDisabled) {
|
if (totalViolations > acknowledgedViolationCount && !sosAutoTriggerDisabled) {
|
||||||
sosDismissed = false;
|
sosDismissed = false;
|
||||||
sosBtn.style.animation = 'sosPulse 1.2s infinite alternate';
|
sosBtn.style.animation = 'sosPulse 1.2s infinite alternate';
|
||||||
sosBtn.innerText = '🚨 candidate might cheating';
|
const hasExternalAi = currentViolations.some(v => v.type === 'external_ai_detected');
|
||||||
|
const hasTabSwitch = currentViolations.some(v => v.type === 'tab_switch');
|
||||||
|
const hasPhoneTalk = currentViolations.some(v => v.type === 'talking_on_phone');
|
||||||
|
if (hasExternalAi) {
|
||||||
|
sosBtn.innerText = '🚨 candidate might use external ai';
|
||||||
sosBtn.style.background = 'linear-gradient(135deg, #ef4444 0%, #b91c1c 100%)';
|
sosBtn.style.background = 'linear-gradient(135deg, #ef4444 0%, #b91c1c 100%)';
|
||||||
|
} else if (hasPhoneTalk) {
|
||||||
|
sosBtn.innerText = '📞 Candidate Talking on Phone';
|
||||||
|
sosBtn.style.background = 'linear-gradient(135deg, #ef4444 0%, #b91c1c 100%)';
|
||||||
|
} else if (hasTabSwitch) {
|
||||||
|
sosBtn.innerText = '📸 Candidate Switched Browser Tab';
|
||||||
|
sosBtn.style.background = 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)';
|
||||||
|
} else {
|
||||||
|
sosBtn.innerText = '🚨 Candidate Proctoring Violation Alert';
|
||||||
|
sosBtn.style.background = 'linear-gradient(135deg, #ef4444 0%, #b91c1c 100%)';
|
||||||
|
}
|
||||||
openSosModal();
|
openSosModal();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -1565,9 +1580,23 @@ function openSosModal() {
|
|||||||
let html = '';
|
let html = '';
|
||||||
if (activeViolations.length > 0) {
|
if (activeViolations.length > 0) {
|
||||||
activeViolations.forEach(v => {
|
activeViolations.forEach(v => {
|
||||||
html += `<div style="margin-bottom: 8px;">
|
let typeTitle = v.type.toUpperCase();
|
||||||
<strong>🚨 ${v.type.toUpperCase()}:</strong> ${v.details || 'Suspicious candidate behavior detected.'}
|
if (v.type === 'external_ai_detected') {
|
||||||
<div style="font-size: 0.65rem; color: #94a3b8;">Timestamp: ${new Date(v.timestamp).toLocaleTimeString()}</div>
|
typeTitle = 'candidate might use external ai';
|
||||||
|
} else if (v.type === 'tab_switch') {
|
||||||
|
typeTitle = 'Candidate Switched Browser Tab';
|
||||||
|
} else if (v.type === 'talking_on_phone') {
|
||||||
|
typeTitle = 'Candidate Talking on Phone';
|
||||||
|
} else if (v.type === 'reading_external_device') {
|
||||||
|
typeTitle = 'Candidate Reading from External Device';
|
||||||
|
} else if (v.type === 'question_repetition' || v.type === 'question_repeat_lower') {
|
||||||
|
typeTitle = 'Candidate Reading / Repeating Question Out Loud';
|
||||||
|
} else if (v.type === 'gaze_anomaly' || v.type === 'gaze_lower_device') {
|
||||||
|
typeTitle = 'Candidate Lower Eye Gaze Detected';
|
||||||
|
}
|
||||||
|
html += `<div style="margin-bottom: 10px; padding: 8px; background: rgba(255,255,255,0.04); border-radius: 6px; border-left: 3px solid ${v.type === 'external_ai_detected' ? '#ef4444' : '#f59e0b'};">
|
||||||
|
<strong style="color: ${v.type === 'external_ai_detected' ? '#fca5a5' : '#fde047'};">🚨 ${typeTitle}:</strong> ${v.details || 'Suspicious candidate activity detected.'}
|
||||||
|
<div style="font-size: 0.65rem; color: #94a3b8; margin-top: 3px;">Timestamp: ${new Date(v.timestamp).toLocaleTimeString()}</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -361,4 +361,55 @@ public function test_silent_proctoring_violations_and_multi_party_active_call_st
|
|||||||
$activeCallsResp->assertStatus(200)
|
$activeCallsResp->assertStatus(200)
|
||||||
->assertJsonFragment(['submission_unique_id' => 'proctor-test-candidate_9555544444_1752000006']);
|
->assertJsonFragment(['submission_unique_id' => 'proctor-test-candidate_9555544444_1752000006']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_tab_switch_logging_and_external_ai_detection_behavior(): void
|
||||||
|
{
|
||||||
|
$interview = Interview::create([
|
||||||
|
'candidate_name' => 'Tab Switch Candidate',
|
||||||
|
'candidate_email' => 'tabswitch@gmail.com',
|
||||||
|
'candidate_phone' => '9998887776',
|
||||||
|
'temp_password' => 'Pass-999888',
|
||||||
|
'expires_at' => now()->addHours(2),
|
||||||
|
'language' => 'python',
|
||||||
|
'status' => 'in_progress',
|
||||||
|
'submission_unique_id' => 'tab-switch-candidate_9998887776_1752000099',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 1. Tab switch log violation should log type 'tab_switch'
|
||||||
|
$resp = $this->post(route('interview.violation', $interview->id), [
|
||||||
|
'type' => 'tab_switch',
|
||||||
|
'details' => 'Candidate switched browser tab or window',
|
||||||
|
]);
|
||||||
|
$resp->assertStatus(200);
|
||||||
|
|
||||||
|
$poll = $this->get(route('interview.poll', $interview->id));
|
||||||
|
$poll->assertStatus(200)
|
||||||
|
->assertJsonFragment(['type' => 'tab_switch', 'details' => 'Candidate switched browser tab or window']);
|
||||||
|
|
||||||
|
// 2. External AI hotkey violation should log type 'external_ai_detected' with meaningful message
|
||||||
|
$aiResp = $this->post(route('interview.violation', $interview->id), [
|
||||||
|
'type' => 'external_ai_detected',
|
||||||
|
'details' => 'candidate might use external ai: Intercepted AI assistant shortcut hotkey (KeyA)',
|
||||||
|
]);
|
||||||
|
$aiResp->assertStatus(200);
|
||||||
|
|
||||||
|
// 3. Talking on phone violation
|
||||||
|
$phoneResp = $this->post(route('interview.violation', $interview->id), [
|
||||||
|
'type' => 'talking_on_phone',
|
||||||
|
'details' => 'Candidate detected talking on phone / phone call during assessment: "hello hold on calling you back"',
|
||||||
|
]);
|
||||||
|
$phoneResp->assertStatus(200);
|
||||||
|
|
||||||
|
// 4. Reading from external device violation
|
||||||
|
$extDevResp = $this->post(route('interview.violation', $interview->id), [
|
||||||
|
'type' => 'reading_external_device',
|
||||||
|
'details' => 'Candidate detected reading from external device, mobile screen, or secondary monitor',
|
||||||
|
]);
|
||||||
|
$extDevResp->assertStatus(200);
|
||||||
|
|
||||||
|
$poll3 = $this->get(route('interview.poll', $interview->id));
|
||||||
|
$poll3->assertStatus(200)
|
||||||
|
->assertJsonFragment(['type' => 'talking_on_phone'])
|
||||||
|
->assertJsonFragment(['type' => 'reading_external_device']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user