Spaces:
Sleeping
Sleeping
| let puzzleStartTime = null; | |
| document.addEventListener('DOMContentLoaded', () => { | |
| // DOM elements | |
| const submitBtn = document.getElementById('submit-answer'); | |
| const userAnswerInput = document.getElementById('user-answer'); | |
| const puzzleImage = document.getElementById('puzzle-image'); | |
| const puzzleImageContainer = document.querySelector('.puzzle-image-container'); | |
| const resultMessage = document.getElementById('result-message'); | |
| const totalCount = document.getElementById('total-count'); | |
| const correctCount = document.getElementById('correct-count'); | |
| const accuracyEl = document.getElementById('accuracy'); | |
| const puzzlePrompt = document.getElementById('puzzle-prompt'); | |
| const puzzleContainer = document.getElementById('puzzle-container'); | |
| const inputGroup = document.querySelector('.input-group'); | |
| const difficultyStars = document.getElementById('difficulty-stars'); | |
| // Debug mode - set to true to show ground truth areas | |
| const DEBUG_MODE = false; | |
| // Tracking state | |
| let currentPuzzle = null; | |
| let benchmarkStats = { | |
| total: 0, | |
| correct: 0 | |
| }; | |
| let clickCoordinates = null; | |
| let processingClick = false; // Flag to prevent multiple clicks while processing | |
| let currentRotationAngle = 0; // Track current rotation for Rotation_Match | |
| let selectedCells = []; // Track selected cells for Unusual_Detection | |
| let bingoSelectedCells = []; // Track selected cells for Bingo swap | |
| let selectedAnimalIndex = -1; // Track selected animal index for Select_Animal | |
| // Add debug type tracking variable | |
| // let debugPuzzleType = null; | |
| // Initialize difficulty stars with default value (to show something immediately) | |
| displayDifficultyStars('Dice_Count'); | |
| // Event listeners | |
| submitBtn.addEventListener('click', submitAnswer); | |
| userAnswerInput.addEventListener('keypress', (e) => { | |
| if (e.key === 'Enter') { | |
| submitAnswer(); | |
| } | |
| }); | |
| // Add click event handler directly to the puzzle image | |
| puzzleImage.addEventListener('click', handleImageClick); | |
| // Add debug mode selector | |
| // setupDebugModeSelector(); | |
| // Functions | |
| function handleImageClick(e) { | |
| if (currentPuzzle && currentPuzzle.input_type === 'click' && !processingClick) { | |
| // Prevent multiple clicks while processing | |
| processingClick = true; | |
| // Get click coordinates relative to the image | |
| const rect = e.target.getBoundingClientRect(); | |
| const x = Math.round(e.clientX - rect.left); | |
| const y = Math.round(e.clientY - rect.top); | |
| // Store coordinates for submission | |
| clickCoordinates = [x, y]; | |
| // Show where user clicked | |
| showClickMarker(x, y); | |
| // Log for debugging | |
| console.log('Click received:', { x, y, target: e.target.id }); | |
| // Special handling for Misleading_Click to show if click is in avoid area | |
| if (currentPuzzle.puzzle_type === 'Misleading_Click' && currentPuzzle.avoid_area) { | |
| const { x: areaX, y: areaY, width: areaWidth, height: areaHeight } = currentPuzzle.avoid_area; | |
| // Check if click is within the avoid area | |
| const inAvoidArea = ( | |
| areaX <= x && x <= areaX + areaWidth && | |
| areaY <= y && y <= areaY + areaHeight | |
| ); | |
| if (inAvoidArea) { | |
| console.log('Click is inside the avoid area! This is incorrect.'); | |
| // Add a visual indicator | |
| const marker = document.querySelector('.click-marker'); | |
| if (marker) { | |
| marker.style.borderColor = 'red'; | |
| marker.style.backgroundColor = 'rgba(255, 0, 0, 0.7)'; | |
| } | |
| } else { | |
| console.log('Click is outside the avoid area! This is correct.'); | |
| // Add a visual indicator | |
| const marker = document.querySelector('.click-marker'); | |
| if (marker) { | |
| marker.style.borderColor = 'green'; | |
| marker.style.backgroundColor = 'rgba(0, 255, 0, 0.7)'; | |
| } | |
| } | |
| } | |
| // Special handling for Pick_Area to show if click is in the target area | |
| else if (currentPuzzle.puzzle_type === 'Pick_Area') { | |
| // Get the ground truth data to validate the click | |
| fetch('/api/get_ground_truth', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify({ | |
| puzzle_type: currentPuzzle.puzzle_type, | |
| puzzle_id: currentPuzzle.puzzle_id | |
| }) | |
| }) | |
| .then(response => response.json()) | |
| .then(gtData => { | |
| if (gtData.answer && gtData.answer.area) { | |
| // Extract area boundaries from the ground truth | |
| const [[minX, minY], [maxX, maxY]] = gtData.answer.area; | |
| // Basic rectangular check | |
| const inRectArea = (minX <= x && x <= maxX && minY <= y && y <= maxY); | |
| // For more accurate curve detection: | |
| let inPolygonArea = false; | |
| if (gtData.answer.polygon) { | |
| // If we have a polygon definition for the curved area | |
| inPolygonArea = pointInPolygon(x, y, gtData.answer.polygon); | |
| } | |
| // Determine if the click is in the target area | |
| // Use polygon if available, otherwise fall back to rectangular check | |
| const inArea = gtData.answer.polygon ? inPolygonArea : inRectArea; | |
| // Get the marker element | |
| const marker = document.querySelector('.click-marker'); | |
| if (marker) { | |
| if (inArea) { | |
| console.log('Click is inside the target area! This is correct.'); | |
| marker.style.borderColor = 'green'; | |
| marker.style.backgroundColor = 'rgba(0, 255, 0, 0.7)'; | |
| // Add a success message | |
| const successMsg = document.createElement('div'); | |
| successMsg.className = 'success-msg'; | |
| successMsg.textContent = 'In largest area!'; | |
| successMsg.style.position = 'absolute'; | |
| successMsg.style.top = '-25px'; | |
| successMsg.style.left = '50%'; | |
| successMsg.style.transform = 'translateX(-50%)'; | |
| successMsg.style.backgroundColor = 'rgba(0, 128, 0, 0.9)'; | |
| successMsg.style.color = 'white'; | |
| successMsg.style.padding = '3px 8px'; | |
| successMsg.style.borderRadius = '3px'; | |
| successMsg.style.fontSize = '12px'; | |
| successMsg.style.fontWeight = 'bold'; | |
| marker.appendChild(successMsg); | |
| } else { | |
| console.log('Click is outside the target area! This is incorrect.'); | |
| marker.style.borderColor = 'red'; | |
| marker.style.backgroundColor = 'rgba(255, 0, 0, 0.7)'; | |
| // Add an error message | |
| const errorMsg = document.createElement('div'); | |
| errorMsg.className = 'error-msg'; | |
| errorMsg.textContent = 'Not in largest area!'; | |
| errorMsg.style.position = 'absolute'; | |
| errorMsg.style.top = '-25px'; | |
| errorMsg.style.left = '50%'; | |
| errorMsg.style.transform = 'translateX(-50%)'; | |
| errorMsg.style.backgroundColor = 'rgba(255, 0, 0, 0.9)'; | |
| errorMsg.style.color = 'white'; | |
| errorMsg.style.padding = '3px 8px'; | |
| errorMsg.style.borderRadius = '3px'; | |
| errorMsg.style.fontSize = '12px'; | |
| errorMsg.style.fontWeight = 'bold'; | |
| marker.appendChild(errorMsg); | |
| } | |
| } | |
| } | |
| }) | |
| .catch(error => { | |
| console.error('Error validating click for Pick_Area:', error); | |
| }); | |
| } | |
| // Auto-submit after click | |
| setTimeout(() => { | |
| submitAnswer(); | |
| }, 500); // Increase delay slightly to allow for fetch response | |
| } | |
| } | |
| // Function to handle rotation | |
| function setupRotationControls() { | |
| // Remove any existing controls first | |
| const existingControls = document.querySelector('.rotation-controls'); | |
| if (existingControls) { | |
| existingControls.remove(); | |
| } | |
| // Create rotation controls | |
| const rotationControls = document.createElement('div'); | |
| rotationControls.className = 'rotation-controls'; | |
| // Create left rotation button | |
| const leftBtn = document.createElement('button'); | |
| leftBtn.className = 'rotate-left'; | |
| leftBtn.innerHTML = '↶'; // Counter-clockwise arrow | |
| leftBtn.setAttribute('aria-label', 'Rotate left'); | |
| // Create right rotation button | |
| const rightBtn = document.createElement('button'); | |
| rightBtn.className = 'rotate-right'; | |
| rightBtn.innerHTML = '↷'; // Clockwise arrow | |
| rightBtn.setAttribute('aria-label', 'Rotate right'); | |
| // Add buttons to controls | |
| rotationControls.appendChild(leftBtn); | |
| rotationControls.appendChild(rightBtn); | |
| // Add to puzzle container | |
| const imageWrapper = document.querySelector('.puzzle-image-wrapper'); | |
| // Create a container for the reference image | |
| const referenceContainer = document.createElement('div'); | |
| referenceContainer.className = 'reference-image-container'; | |
| const referenceImg = document.createElement('img'); | |
| referenceImg.id = 'reference-image'; | |
| referenceImg.src = currentPuzzle.reference_image; | |
| referenceImg.alt = 'Reference direction'; | |
| referenceContainer.appendChild(referenceImg); | |
| // Create a container for the object image | |
| const objectContainer = document.createElement('div'); | |
| objectContainer.className = 'object-image-container'; | |
| const objectImg = document.createElement('img'); | |
| objectImg.id = 'object-image'; | |
| objectImg.src = currentPuzzle.object_image; | |
| objectImg.alt = 'Rotatable object'; | |
| objectContainer.appendChild(objectImg); | |
| // Create a two-column layout for rotation puzzle | |
| const rotationLayout = document.createElement('div'); | |
| rotationLayout.className = 'rotation-layout'; | |
| rotationLayout.appendChild(referenceContainer); | |
| rotationLayout.appendChild(objectContainer); | |
| // Replace the existing puzzle image | |
| puzzleImageContainer.innerHTML = ''; | |
| puzzleImageContainer.appendChild(rotationLayout); | |
| // Add rotation controls below the image | |
| imageWrapper.appendChild(rotationControls); | |
| // Add event listeners for rotation buttons | |
| leftBtn.addEventListener('click', () => rotateObject(-45)); | |
| rightBtn.addEventListener('click', () => rotateObject(45)); | |
| // Set initial angle | |
| currentRotationAngle = currentPuzzle.current_angle || 0; | |
| updateObjectRotation(); | |
| } | |
| function rotateObject(angleDelta) { | |
| // Update the current angle | |
| currentRotationAngle = (currentRotationAngle + angleDelta) % 360; | |
| if (currentRotationAngle < 0) { | |
| currentRotationAngle += 360; | |
| } | |
| // Apply the rotation | |
| updateObjectRotation(); | |
| // Log for debugging | |
| console.log('Rotated to:', currentRotationAngle); | |
| } | |
| function updateObjectRotation() { | |
| const objectImg = document.getElementById('object-image'); | |
| if (objectImg) { | |
| // Option 1: Use CSS transform to rotate the image | |
| objectImg.style.transform = `rotate(${currentRotationAngle}deg)`; | |
| // Option 2: Load a pre-rotated image if available | |
| // This would require having images at each rotation angle | |
| const baseName = currentPuzzle.object_base; | |
| // Find the closest pre-rotated image (0, 90, 180, 270) | |
| const angles = [0, 45, 90, 135, 180, 225, 270, 315]; | |
| const closestAngle = angles.reduce((prev, curr) => | |
| Math.abs(curr - currentRotationAngle) < Math.abs(prev - currentRotationAngle) ? curr : prev | |
| ); | |
| // Load the pre-rotated image | |
| const rotatedImagePath = `/captcha_data/${currentPuzzle.puzzle_type}/${baseName}_${closestAngle}.png`; | |
| objectImg.src = rotatedImagePath; | |
| // Apply any additional rotation needed | |
| const remainingRotation = currentRotationAngle - closestAngle; | |
| if (remainingRotation !== 0) { | |
| objectImg.style.transform = `rotate(${remainingRotation}deg)`; | |
| } else { | |
| objectImg.style.transform = 'none'; | |
| } | |
| } | |
| } | |
| // Function to set up sliding puzzle | |
| function setupSlidePuzzle() { | |
| // Remove any existing controls first | |
| const existingSlider = document.querySelector('.slider-component'); | |
| if (existingSlider) { | |
| existingSlider.remove(); | |
| } | |
| // Clear the puzzle image container | |
| puzzleImageContainer.innerHTML = ''; | |
| // Create a container for the background image | |
| const backgroundContainer = document.createElement('div'); | |
| backgroundContainer.className = 'background-container'; | |
| backgroundContainer.style.position = 'relative'; | |
| backgroundContainer.style.width = '100%'; | |
| backgroundContainer.style.height = 'auto'; | |
| // Add background image | |
| const backgroundImg = document.createElement('img'); | |
| backgroundImg.src = currentPuzzle.background_image; | |
| backgroundImg.alt = 'Slide puzzle background'; | |
| backgroundImg.style.width = '100%'; | |
| backgroundImg.style.height = 'auto'; | |
| backgroundImg.style.display = 'block'; | |
| backgroundContainer.appendChild(backgroundImg); | |
| // Create draggable slider component | |
| const sliderComponent = document.createElement('div'); | |
| sliderComponent.className = 'slider-component'; | |
| sliderComponent.style.position = 'absolute'; | |
| sliderComponent.style.cursor = 'move'; | |
| sliderComponent.style.zIndex = '10'; | |
| sliderComponent.style.userSelect = 'none'; | |
| sliderComponent.style.touchAction = 'none'; | |
| sliderComponent.style.width = '50px'; | |
| // Add component image | |
| const componentImg = document.createElement('img'); | |
| componentImg.src = currentPuzzle.component_image; | |
| componentImg.alt = 'Slide component'; | |
| componentImg.style.width = '150%'; | |
| componentImg.style.height = 'auto'; | |
| componentImg.style.display = 'block'; | |
| componentImg.draggable = false; // Prevent default dragging behavior | |
| sliderComponent.appendChild(componentImg); | |
| // Add slider component to the background container | |
| backgroundContainer.appendChild(sliderComponent); | |
| // Add the whole setup to the puzzle image container | |
| puzzleImageContainer.appendChild(backgroundContainer); | |
| // Wait for images to load to get proper dimensions | |
| backgroundImg.onload = () => { | |
| // Get container dimensions | |
| const containerWidth = backgroundImg.width; | |
| const containerHeight = backgroundImg.height; | |
| // Load component image to get its dimensions | |
| componentImg.onload = () => { | |
| const originalComponentWidth = componentImg.naturalWidth; | |
| const originalComponentHeight = componentImg.naturalHeight; | |
| const componentWidth = containerWidth * 0.08; | |
| const aspectRatio = originalComponentWidth / originalComponentHeight; | |
| const componentHeight = componentWidth / aspectRatio; | |
| sliderComponent.style.width = `${componentWidth}px`; | |
| // Initial position for the slider component - bottom right corner (far from typical target) | |
| const initialLeft = containerWidth - componentWidth - 20; | |
| const initialTop = containerHeight - componentHeight - 20; | |
| sliderComponent.style.left = `${initialLeft}px`; | |
| sliderComponent.style.top = `${initialTop}px`; | |
| // Initialize current position tracking variables | |
| currentX = initialLeft; | |
| currentY = initialTop; | |
| // In debug mode, fetch and show the target area | |
| if (DEBUG_MODE) { | |
| fetch('/api/get_ground_truth', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify({ | |
| puzzle_type: currentPuzzle.puzzle_type, | |
| puzzle_id: currentPuzzle.puzzle_id | |
| }) | |
| }) | |
| .then(response => response.json()) | |
| .then(gtData => { | |
| if (gtData.answer) { | |
| // Get tolerance value if available | |
| const tolerance = gtData.answer.tolerance || 15; // Default to 15px | |
| showSliderTargetArea(gtData.answer, backgroundContainer, tolerance); | |
| } | |
| }) | |
| .catch(error => { | |
| console.error('Error fetching ground truth:', error); | |
| }); | |
| } | |
| }; | |
| }; | |
| // Set up draggable functionality | |
| let isDragging = false; | |
| let startX, startY, startLeft, startTop; | |
| // Track current position | |
| let currentX = 0; | |
| let currentY = 0; | |
| // Mouse events for desktop | |
| sliderComponent.addEventListener('mousedown', (e) => { | |
| isDragging = true; | |
| startX = e.clientX; | |
| startY = e.clientY; | |
| startLeft = parseInt(sliderComponent.style.left) || 0; | |
| startTop = parseInt(sliderComponent.style.top) || 0; | |
| sliderComponent.style.opacity = '0.8'; | |
| // Prevent default browser behavior | |
| e.preventDefault(); | |
| }); | |
| document.addEventListener('mousemove', (e) => { | |
| if (!isDragging) return; | |
| // Calculate new position | |
| const deltaX = e.clientX - startX; | |
| const deltaY = e.clientY - startY; | |
| // Calculate new position | |
| let newLeft = startLeft + deltaX; | |
| let newTop = startTop + deltaY; | |
| // Get container dimensions | |
| const containerRect = backgroundContainer.getBoundingClientRect(); | |
| const sliderRect = sliderComponent.getBoundingClientRect(); | |
| // Ensure the slider stays within the container bounds | |
| if (newLeft < 0) newLeft = 0; | |
| if (newTop < 0) newTop = 0; | |
| if (newLeft > containerRect.width - sliderRect.width) | |
| newLeft = containerRect.width - sliderRect.width; | |
| if (newTop > containerRect.height - sliderRect.height) | |
| newTop = containerRect.height - sliderRect.height; | |
| sliderComponent.style.left = `${newLeft}px`; | |
| sliderComponent.style.top = `${newTop}px`; | |
| // Update current position | |
| currentX = newLeft; | |
| currentY = newTop; | |
| }); | |
| document.addEventListener('mouseup', () => { | |
| if (isDragging) { | |
| isDragging = false; | |
| sliderComponent.style.opacity = '1'; | |
| // Calculate center point | |
| const componentRect = componentImg.getBoundingClientRect(); | |
| const centerX = currentX + (componentRect.width / 2); | |
| const centerY = currentY + (componentRect.height / 2); | |
| // Log final position for debugging | |
| console.log('Slider final position (top-left):', { x: currentX, y: currentY }); | |
| console.log('Slider center position:', { x: centerX, y: centerY }); | |
| } | |
| }); | |
| // Touch events for mobile | |
| sliderComponent.addEventListener('touchstart', (e) => { | |
| isDragging = true; | |
| startX = e.touches[0].clientX; | |
| startY = e.touches[0].clientY; | |
| startLeft = parseInt(sliderComponent.style.left) || 0; | |
| startTop = parseInt(sliderComponent.style.top) || 0; | |
| sliderComponent.style.opacity = '0.8'; | |
| // Prevent default browser behavior | |
| e.preventDefault(); | |
| }); | |
| document.addEventListener('touchmove', (e) => { | |
| if (!isDragging) return; | |
| // Calculate new position | |
| const deltaX = e.touches[0].clientX - startX; | |
| const deltaY = e.touches[0].clientY - startY; | |
| // Calculate new position | |
| let newLeft = startLeft + deltaX; | |
| let newTop = startTop + deltaY; | |
| // Get container dimensions | |
| const containerRect = backgroundContainer.getBoundingClientRect(); | |
| const sliderRect = sliderComponent.getBoundingClientRect(); | |
| // Ensure the slider stays within the container bounds | |
| if (newLeft < 0) newLeft = 0; | |
| if (newTop < 0) newTop = 0; | |
| if (newLeft > containerRect.width - sliderRect.width) | |
| newLeft = containerRect.width - sliderRect.width; | |
| if (newTop > containerRect.height - sliderRect.height) | |
| newTop = containerRect.height - sliderRect.height; | |
| sliderComponent.style.left = `${newLeft}px`; | |
| sliderComponent.style.top = `${newTop}px`; | |
| // Update current position | |
| currentX = newLeft; | |
| currentY = newTop; | |
| }); | |
| document.addEventListener('touchend', () => { | |
| if (isDragging) { | |
| isDragging = false; | |
| sliderComponent.style.opacity = '1'; | |
| // Calculate center point | |
| const componentRect = componentImg.getBoundingClientRect(); | |
| const centerX = currentX + (componentRect.width / 2); | |
| const centerY = currentY + (componentRect.height / 2); | |
| // Log final position for debugging | |
| console.log('Slider final position (top-left):', { x: currentX, y: currentY }); | |
| console.log('Slider center position:', { x: centerX, y: centerY }); | |
| } | |
| }); | |
| // Add submit button for the sliding puzzle | |
| const submitSection = document.createElement('div'); | |
| submitSection.className = 'slider-submit'; | |
| const sliderSubmitBtn = document.createElement('button'); | |
| sliderSubmitBtn.textContent = 'Submit'; | |
| sliderSubmitBtn.className = 'submit-slider'; | |
| sliderSubmitBtn.addEventListener('click', () => { | |
| // When submitting, we need to get the final position | |
| // and normalize it to the image dimensions | |
| const componentRect = componentImg.getBoundingClientRect(); | |
| // Calculate center point of the component | |
| const centerX = currentX + (componentRect.width / 2); | |
| const centerY = currentY + (componentRect.height / 2); | |
| // Submit this position | |
| console.log('Submitting slider position:', { x: centerX, y: centerY }); | |
| submitSliderPosition(centerX, centerY); | |
| }); | |
| submitSection.appendChild(sliderSubmitBtn); | |
| // Add to puzzle container | |
| const imageWrapper = document.querySelector('.puzzle-image-wrapper'); | |
| imageWrapper.appendChild(submitSection); | |
| } | |
| // Function to show the target area for the slider in debug mode | |
| function showSliderTargetArea(targetPosition, container, tolerance = 15) { | |
| if (!DEBUG_MODE || !targetPosition) return; | |
| // Remove any existing debug targets | |
| const existingTarget = document.querySelector('.target-area'); | |
| if (existingTarget) { | |
| existingTarget.remove(); | |
| } | |
| // Create a target element | |
| const targetArea = document.createElement('div'); | |
| targetArea.className = 'target-area'; | |
| // Get target coordinates | |
| const [targetX, targetY] = targetPosition; | |
| // We'll visualize this as a circle | |
| const diameter = tolerance * 2; | |
| // Style the target area | |
| targetArea.style.position = 'absolute'; | |
| targetArea.style.left = `${targetX - tolerance}px`; | |
| targetArea.style.top = `${targetY - tolerance}px`; | |
| targetArea.style.width = `${diameter}px`; | |
| targetArea.style.height = `${diameter}px`; | |
| targetArea.style.borderRadius = '50%'; | |
| targetArea.style.border = '2px dashed green'; | |
| targetArea.style.backgroundColor = 'rgba(0, 255, 0, 0.2)'; | |
| targetArea.style.zIndex = '5'; | |
| targetArea.style.pointerEvents = 'none'; // Allow clicks to pass through | |
| // Add coordinates label | |
| const coordsLabel = document.createElement('div'); | |
| coordsLabel.className = 'coords-label'; | |
| coordsLabel.textContent = `Target: (${targetX}, ${targetY}) ±${tolerance}px`; | |
| coordsLabel.style.position = 'absolute'; | |
| coordsLabel.style.top = '-25px'; | |
| coordsLabel.style.left = '0'; | |
| coordsLabel.style.backgroundColor = 'rgba(0, 0, 0, 0.7)'; | |
| coordsLabel.style.color = 'white'; | |
| coordsLabel.style.padding = '2px 5px'; | |
| coordsLabel.style.fontSize = '10px'; | |
| coordsLabel.style.borderRadius = '3px'; | |
| coordsLabel.style.whiteSpace = 'nowrap'; | |
| targetArea.appendChild(coordsLabel); | |
| // Add to the container | |
| container.appendChild(targetArea); | |
| // Log the target details | |
| console.log('Target position:', { | |
| x: targetX, | |
| y: targetY, | |
| tolerance: tolerance | |
| }); | |
| } | |
| // Function to submit slider position | |
| function submitSliderPosition(x, y) { | |
| if (!currentPuzzle) { | |
| resultMessage.textContent = 'Loading puzzle, please wait...'; | |
| resultMessage.className = 'result-message incorrect'; | |
| return; | |
| } | |
| // Send position to the server for verification | |
| fetch('/api/check_answer', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify({ | |
| puzzle_type: currentPuzzle.puzzle_type, | |
| puzzle_id: currentPuzzle.puzzle_id, | |
| answer: [x, y] | |
| }) | |
| }) | |
| .then(response => response.json()) | |
| .then(data => { | |
| // Update stats | |
| benchmarkStats.total++; | |
| if (data.correct) { | |
| benchmarkStats.correct++; | |
| resultMessage.textContent = 'Correct! The slider was placed in the right position.'; | |
| resultMessage.className = 'result-message correct'; | |
| } else { | |
| resultMessage.textContent = 'Incorrect. Please try again with a better position.'; | |
| resultMessage.className = 'result-message incorrect'; | |
| } | |
| updateStats(); | |
| // Record benchmark result | |
| recordBenchmarkResult({ | |
| puzzle_type: currentPuzzle.puzzle_type, | |
| puzzle_id: currentPuzzle.puzzle_id, | |
| user_answer: [x, y], | |
| correct_answer: data.correct_answer, | |
| correct: data.correct | |
| }); | |
| // Disable the submit button to prevent multiple submissions | |
| const submitBtn = document.querySelector('.submit-slider'); | |
| if (submitBtn) { | |
| submitBtn.disabled = true; | |
| } | |
| // Also disable rotation submit button if it exists | |
| const rotateSubmitBtn = document.querySelector('.submit-rotation'); | |
| if (rotateSubmitBtn) { | |
| rotateSubmitBtn.disabled = true; | |
| } | |
| // Also disable image recognition submit button if it exists | |
| const imageRecognitionSubmitBtn = document.querySelector('.submit-image-recognition'); | |
| if (imageRecognitionSubmitBtn) { | |
| imageRecognitionSubmitBtn.disabled = true; | |
| } | |
| // Also disable bingo submit button if it exists | |
| const bingoSubmitBtn = document.querySelector('.submit-bingo'); | |
| if (bingoSubmitBtn) { | |
| bingoSubmitBtn.disabled = true; | |
| } | |
| // Also disable image matching submit button if it exists | |
| const imageMatchingSubmitBtn = document.querySelector('.submit-image-matching'); | |
| if (imageMatchingSubmitBtn) { | |
| imageMatchingSubmitBtn.disabled = true; | |
| } | |
| // Load a new puzzle after a delay | |
| setTimeout(loadNewPuzzle, 2000); | |
| }) | |
| .catch(error => { | |
| console.error('Error checking answer:', error); | |
| resultMessage.textContent = 'Error checking answer. Please try again.'; | |
| resultMessage.className = 'result-message incorrect'; | |
| }); | |
| } | |
| // Add this new function to show the ground truth area | |
| function showGroundTruthArea(answer) { | |
| if (!DEBUG_MODE) return; | |
| // Remove any existing debug areas | |
| const existingArea = document.querySelector('.debug-area'); | |
| if (existingArea) { | |
| existingArea.remove(); | |
| } | |
| // Create and style the debug area element | |
| const debugArea = document.createElement('div'); | |
| debugArea.className = 'debug-area'; | |
| debugArea.style.position = 'absolute'; | |
| debugArea.style.pointerEvents = 'none'; // Allow clicks to pass through | |
| debugArea.style.zIndex = '5'; | |
| if (answer && answer.area) { | |
| // For standard area format (geometry_click, etc.) | |
| const [[x1, y1], [x2, y2]] = answer.area; | |
| debugArea.style.left = `${x1}px`; | |
| debugArea.style.top = `${y1}px`; | |
| debugArea.style.width = `${x2 - x1}px`; | |
| debugArea.style.height = `${y2 - y1}px`; | |
| debugArea.style.border = '2px dashed yellow'; | |
| debugArea.style.backgroundColor = 'rgba(255, 255, 0, 0.2)'; | |
| // Add coordinates label | |
| const coordsLabel = document.createElement('div'); | |
| coordsLabel.className = 'coords-label'; | |
| coordsLabel.textContent = `TL: (${x1},${y1}) BR: (${x2},${y2})`; | |
| coordsLabel.style.position = 'absolute'; | |
| coordsLabel.style.bottom = '0'; | |
| coordsLabel.style.right = '0'; | |
| coordsLabel.style.backgroundColor = 'rgba(0, 0, 0, 0.7)'; | |
| coordsLabel.style.color = 'white'; | |
| coordsLabel.style.padding = '2px 5px'; | |
| coordsLabel.style.fontSize = '10px'; | |
| coordsLabel.style.borderRadius = '3px'; | |
| debugArea.appendChild(coordsLabel); | |
| // Log the area details | |
| console.log('Ground truth area:', { | |
| topLeft: [x1, y1], | |
| bottomRight: [x2, y2], | |
| width: x2 - x1, | |
| height: y2 - y1, | |
| type: answer.type | |
| }); | |
| } else if (answer && answer.avoid_area) { | |
| // For Misleading_Click avoid_area format | |
| const { x, y, width, height } = answer.avoid_area; | |
| debugArea.style.left = `${x}px`; | |
| debugArea.style.top = `${y}px`; | |
| debugArea.style.width = `${width}px`; | |
| debugArea.style.height = `${height}px`; | |
| debugArea.style.border = '3px dashed red'; | |
| debugArea.style.backgroundColor = 'rgba(255, 0, 0, 0.3)'; | |
| // Add coordinates label | |
| const coordsLabel = document.createElement('div'); | |
| coordsLabel.className = 'coords-label'; | |
| coordsLabel.textContent = `Avoid Area: (${x},${y}) ${width}x${height}`; | |
| coordsLabel.style.position = 'absolute'; | |
| coordsLabel.style.bottom = '0'; | |
| coordsLabel.style.right = '0'; | |
| coordsLabel.style.backgroundColor = 'rgba(0, 0, 0, 0.7)'; | |
| coordsLabel.style.color = 'white'; | |
| coordsLabel.style.padding = '2px 5px'; | |
| coordsLabel.style.fontSize = '10px'; | |
| coordsLabel.style.borderRadius = '3px'; | |
| debugArea.appendChild(coordsLabel); | |
| // Add a "DO NOT CLICK HERE" sign in the middle of the area | |
| const warningSign = document.createElement('div'); | |
| warningSign.className = 'warning-sign'; | |
| warningSign.textContent = 'DO NOT CLICK HERE'; | |
| warningSign.style.position = 'absolute'; | |
| warningSign.style.top = '50%'; | |
| warningSign.style.left = '50%'; | |
| warningSign.style.transform = 'translate(-50%, -50%)'; | |
| warningSign.style.backgroundColor = 'rgba(0, 0, 0, 0.7)'; | |
| warningSign.style.color = '#ff5555'; | |
| warningSign.style.padding = '5px 10px'; | |
| warningSign.style.fontSize = '12px'; | |
| warningSign.style.fontWeight = 'bold'; | |
| warningSign.style.borderRadius = '3px'; | |
| warningSign.style.whiteSpace = 'nowrap'; | |
| warningSign.style.zIndex = '10'; | |
| debugArea.appendChild(warningSign); | |
| // Log the area details | |
| console.log('Avoid area:', { x, y, width, height }); | |
| } else { | |
| // If we don't have a valid format, don't show anything | |
| return; | |
| } | |
| // Add to the image container | |
| puzzleImageContainer.appendChild(debugArea); | |
| } | |
| // Function to fetch and show geometry click target area | |
| function fetchAndShowGeometryClickArea(container) { | |
| if (!DEBUG_MODE || !currentPuzzle) return; | |
| // Fetch ground truth data to show the correct geometric shape area | |
| fetch('/api/get_ground_truth', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify({ | |
| puzzle_type: currentPuzzle.puzzle_type, | |
| puzzle_id: currentPuzzle.puzzle_id | |
| }) | |
| }) | |
| .then(response => response.json()) | |
| .then(gtData => { | |
| if (gtData.answer) { | |
| // Call showGroundTruthArea with the answer data | |
| showGroundTruthArea(gtData.answer); | |
| // Log for debugging | |
| console.log('Geometry_Click ground truth fetched:', gtData.answer); | |
| } | |
| }) | |
| .catch(error => { | |
| console.error('Error fetching ground truth for Geometry_Click:', error); | |
| }); | |
| } | |
| function showClickMarker(x, y) { | |
| // Remove any existing markers | |
| const existingMarker = document.querySelector('.click-marker'); | |
| if (existingMarker) { | |
| existingMarker.remove(); | |
| } | |
| // Create and add new marker | |
| const marker = document.createElement('div'); | |
| marker.className = 'click-marker'; | |
| marker.style.left = `${x}px`; | |
| marker.style.top = `${y}px`; | |
| // Add coordinates label to the marker | |
| const coordsLabel = document.createElement('div'); | |
| coordsLabel.className = 'coords-label'; | |
| coordsLabel.textContent = `(${x},${y})`; | |
| coordsLabel.style.position = 'absolute'; | |
| coordsLabel.style.top = '20px'; | |
| coordsLabel.style.left = '20px'; | |
| coordsLabel.style.backgroundColor = 'rgba(0, 0, 0, 0.7)'; | |
| coordsLabel.style.color = 'white'; | |
| coordsLabel.style.padding = '2px 5px'; | |
| coordsLabel.style.fontSize = '10px'; | |
| coordsLabel.style.borderRadius = '3px'; | |
| coordsLabel.style.whiteSpace = 'nowrap'; | |
| marker.appendChild(coordsLabel); | |
| // Add it directly to the image container for proper positioning | |
| puzzleImageContainer.appendChild(marker); | |
| // Log for debugging | |
| console.log('Marker placed at:', { x, y }); | |
| // Check if this is a Misleading_Click puzzle and we're in debug mode | |
| if (DEBUG_MODE && currentPuzzle && currentPuzzle.puzzle_type === 'Misleading_Click' && currentPuzzle.avoid_area) { | |
| // Get the avoid area | |
| const { x: areaX, y: areaY, width: areaWidth, height: areaHeight } = currentPuzzle.avoid_area; | |
| // Check if click is within the avoid area | |
| const inAvoidArea = ( | |
| areaX <= x && x <= areaX + areaWidth && | |
| areaY <= y && y <= areaY + areaHeight | |
| ); | |
| // Add status indicator | |
| const statusIndicator = document.createElement('div'); | |
| statusIndicator.className = 'click-status'; | |
| statusIndicator.style.position = 'absolute'; | |
| statusIndicator.style.top = '40px'; | |
| statusIndicator.style.left = '20px'; | |
| statusIndicator.style.padding = '3px 6px'; | |
| statusIndicator.style.borderRadius = '3px'; | |
| statusIndicator.style.fontSize = '10px'; | |
| statusIndicator.style.fontWeight = 'bold'; | |
| if (inAvoidArea) { | |
| statusIndicator.textContent = 'INSIDE AVOID AREA - WRONG'; | |
| statusIndicator.style.backgroundColor = 'rgba(255, 0, 0, 0.8)'; | |
| statusIndicator.style.color = 'white'; | |
| marker.style.borderColor = 'red'; | |
| } else { | |
| statusIndicator.textContent = 'OUTSIDE AVOID AREA - CORRECT'; | |
| statusIndicator.style.backgroundColor = 'rgba(0, 255, 0, 0.8)'; | |
| statusIndicator.style.color = 'black'; | |
| marker.style.borderColor = 'green'; | |
| } | |
| marker.appendChild(statusIndicator); | |
| // Log result | |
| console.log('Click check:', { inAvoidArea, message: inAvoidArea ? 'INSIDE avoid area (incorrect)' : 'OUTSIDE avoid area (correct)' }); | |
| } | |
| } | |
| // Function to set up unusual detection grid | |
| function setupUnusualDetectionGrid() { | |
| // Remove any existing grid | |
| const existingGrid = document.querySelector('.unusual-detection-grid'); | |
| if (existingGrid) { | |
| existingGrid.remove(); | |
| } | |
| // Clear the puzzle image container | |
| puzzleImageContainer.innerHTML = ''; | |
| // Get the grid dimensions from the current puzzle data | |
| const gridSize = currentPuzzle.grid_size || [2, 3]; // Default to 2x3 if not specified | |
| const [rows, cols] = gridSize; | |
| // Create the grid container | |
| const gridContainer = document.createElement('div'); | |
| gridContainer.className = 'unusual-detection-grid'; | |
| gridContainer.style.display = 'grid'; | |
| gridContainer.style.gridTemplateColumns = `repeat(${cols}, 1fr)`; | |
| gridContainer.style.gridTemplateRows = `repeat(${rows}, 1fr)`; | |
| gridContainer.style.gap = '2px'; | |
| gridContainer.style.width = '100%'; | |
| gridContainer.style.aspectRatio = `${cols} / ${rows}`; | |
| // First, load the full image to get its dimensions | |
| const fullImg = new Image(); | |
| fullImg.onload = () => { | |
| const imgWidth = fullImg.width; | |
| const imgHeight = fullImg.height; | |
| const cellWidth = imgWidth / cols; | |
| const cellHeight = imgHeight / rows; | |
| // Create individual image elements for each cell | |
| const totalCells = rows * cols; | |
| for (let i = 0; i < totalCells; i++) { | |
| const cell = document.createElement('div'); | |
| cell.className = 'grid-cell'; | |
| cell.dataset.index = i; | |
| cell.style.position = 'relative'; | |
| cell.style.border = '2px solid #333'; | |
| cell.style.cursor = 'pointer'; | |
| cell.style.overflow = 'hidden'; | |
| // Create an individual image for this cell | |
| const cellImg = document.createElement('img'); | |
| cellImg.className = 'cell-image'; | |
| cellImg.style.width = '100%'; | |
| cellImg.style.height = '100%'; | |
| cellImg.style.objectFit = 'cover'; | |
| cellImg.style.display = 'block'; | |
| cell.appendChild(cellImg); | |
| // Calculate which part of the source image this cell represents | |
| const row = Math.floor(i / cols); | |
| const col = i % cols; | |
| // Create a canvas to extract just this portion of the image | |
| const canvas = document.createElement('canvas'); | |
| canvas.width = cellWidth; | |
| canvas.height = cellHeight; | |
| const ctx = canvas.getContext('2d'); | |
| // Draw just the portion we want | |
| ctx.drawImage( | |
| fullImg, | |
| col * cellWidth, row * cellHeight, // Source x, y | |
| cellWidth, cellHeight, // Source width, height | |
| 0, 0, // Destination x, y | |
| cellWidth, cellHeight // Destination width, height | |
| ); | |
| // Set the cell image source to this canvas data | |
| cellImg.src = canvas.toDataURL(); | |
| // Create an overlay for selection state | |
| const overlay = document.createElement('div'); | |
| overlay.className = 'cell-overlay'; | |
| overlay.style.position = 'absolute'; | |
| overlay.style.top = '0'; | |
| overlay.style.left = '0'; | |
| overlay.style.width = '100%'; | |
| overlay.style.height = '100%'; | |
| overlay.style.backgroundColor = 'rgba(0, 120, 255, 0.5)'; | |
| overlay.style.opacity = '0'; | |
| overlay.style.transition = 'opacity 0.2s ease'; | |
| overlay.style.pointerEvents = 'none'; | |
| cell.appendChild(overlay); | |
| // Add a checkmark icon to indicate selection | |
| const checkmark = document.createElement('div'); | |
| checkmark.className = 'checkmark'; | |
| checkmark.innerHTML = '✓'; | |
| checkmark.style.position = 'absolute'; | |
| checkmark.style.top = '50%'; | |
| checkmark.style.left = '50%'; | |
| checkmark.style.transform = 'translate(-50%, -50%)'; | |
| checkmark.style.color = 'white'; | |
| checkmark.style.fontSize = '32px'; | |
| checkmark.style.fontWeight = 'bold'; | |
| checkmark.style.opacity = '0'; | |
| checkmark.style.transition = 'opacity 0.2s ease'; | |
| checkmark.style.pointerEvents = 'none'; | |
| cell.appendChild(checkmark); | |
| // Add click event handler for selection | |
| cell.addEventListener('click', (e) => { | |
| toggleCellSelection(i, cell); | |
| }); | |
| // Add the cell to the grid | |
| gridContainer.appendChild(cell); | |
| } | |
| // Add the grid to the puzzle image container | |
| puzzleImageContainer.appendChild(gridContainer); | |
| // Add a submit button below the grid | |
| const submitSection = document.createElement('div'); | |
| submitSection.className = 'unusual-submit'; | |
| submitSection.style.textAlign = 'center'; | |
| submitSection.style.marginTop = '15px'; | |
| const unusualSubmitBtn = document.createElement('button'); | |
| unusualSubmitBtn.textContent = 'Submit'; | |
| unusualSubmitBtn.className = 'submit-unusual'; | |
| unusualSubmitBtn.addEventListener('click', submitAnswer); | |
| submitSection.appendChild(unusualSubmitBtn); | |
| // Add to puzzle container | |
| const imageWrapper = document.querySelector('.puzzle-image-wrapper'); | |
| imageWrapper.appendChild(submitSection); | |
| // Reset selected cells | |
| selectedCells = []; | |
| }; | |
| // Set the source to load the image | |
| fullImg.src = currentPuzzle.image_path; | |
| fullImg.style.display = 'none'; | |
| } | |
| function toggleCellSelection(index, cellElement) { | |
| // Check if this cell is already selected | |
| const isSelected = selectedCells.includes(index); | |
| if (isSelected) { | |
| // Deselect the cell | |
| selectedCells = selectedCells.filter(i => i !== index); | |
| cellElement.querySelector('.cell-overlay').style.opacity = '0'; | |
| cellElement.querySelector('.checkmark').style.opacity = '0'; | |
| cellElement.style.transform = 'scale(1)'; | |
| cellElement.style.borderColor = '#333'; | |
| } else { | |
| // Select the cell | |
| selectedCells.push(index); | |
| cellElement.querySelector('.cell-overlay').style.opacity = '1'; | |
| cellElement.querySelector('.checkmark').style.opacity = '1'; | |
| cellElement.style.transform = 'scale(0.95)'; | |
| cellElement.style.borderColor = '#0078ff'; | |
| } | |
| console.log('Selected cells:', selectedCells); | |
| } | |
| // Function to set up Bingo swap puzzle | |
| function setupBingoSwap() { | |
| // Remove any existing grid | |
| const existingGrid = document.querySelector('.bingo-grid'); | |
| if (existingGrid) { | |
| existingGrid.remove(); | |
| } | |
| // Clear the puzzle image container | |
| puzzleImageContainer.innerHTML = ''; | |
| // Get the grid dimensions from the current puzzle data | |
| const gridSize = currentPuzzle.grid_size || [3, 3]; // Default to 3x3 if not specified | |
| const [rows, cols] = gridSize; | |
| // Create the grid container | |
| const gridContainer = document.createElement('div'); | |
| gridContainer.className = 'bingo-grid'; | |
| gridContainer.style.display = 'grid'; | |
| gridContainer.style.gridTemplateColumns = `repeat(${cols}, 1fr)`; | |
| gridContainer.style.gridTemplateRows = `repeat(${rows}, 1fr)`; | |
| gridContainer.style.gap = '2px'; | |
| gridContainer.style.width = '100%'; | |
| gridContainer.style.aspectRatio = `${cols} / ${rows}`; | |
| // First, load the full image to get its dimensions | |
| const fullImg = new Image(); | |
| fullImg.onload = () => { | |
| const imgWidth = fullImg.width; | |
| const imgHeight = fullImg.height; | |
| const cellWidth = imgWidth / cols; | |
| const cellHeight = imgHeight / rows; | |
| // Create individual image elements for each cell | |
| const totalCells = rows * cols; | |
| for (let i = 0; i < totalCells; i++) { | |
| const cell = document.createElement('div'); | |
| cell.className = 'grid-cell'; | |
| cell.dataset.index = i; | |
| cell.style.position = 'relative'; | |
| cell.style.border = '2px solid #333'; | |
| cell.style.cursor = 'pointer'; | |
| cell.style.overflow = 'hidden'; | |
| // Create an individual image for this cell | |
| const cellImg = document.createElement('img'); | |
| cellImg.className = 'cell-image'; | |
| cellImg.style.width = '100%'; | |
| cellImg.style.height = '100%'; | |
| cellImg.style.objectFit = 'cover'; | |
| cellImg.style.display = 'block'; | |
| cell.appendChild(cellImg); | |
| // Calculate which part of the source image this cell represents | |
| const row = Math.floor(i / cols); | |
| const col = i % cols; | |
| // Create a canvas to extract just this portion of the image | |
| const canvas = document.createElement('canvas'); | |
| canvas.width = cellWidth; | |
| canvas.height = cellHeight; | |
| const ctx = canvas.getContext('2d'); | |
| // Draw just the portion we want | |
| ctx.drawImage( | |
| fullImg, | |
| col * cellWidth, row * cellHeight, // Source x, y | |
| cellWidth, cellHeight, // Source width, height | |
| 0, 0, // Destination x, y | |
| cellWidth, cellHeight // Destination width, height | |
| ); | |
| // Create a data URL and set it as the image source | |
| cellImg.src = canvas.toDataURL(); | |
| // Create an overlay for selection state | |
| const overlay = document.createElement('div'); | |
| overlay.className = 'cell-overlay'; | |
| overlay.style.position = 'absolute'; | |
| overlay.style.top = '0'; | |
| overlay.style.left = '0'; | |
| overlay.style.width = '100%'; | |
| overlay.style.height = '100%'; | |
| overlay.style.backgroundColor = 'rgba(0, 120, 255, 0.5)'; | |
| overlay.style.opacity = '0'; | |
| overlay.style.transition = 'opacity 0.2s ease'; | |
| overlay.style.pointerEvents = 'none'; | |
| cell.appendChild(overlay); | |
| // Add click handler for selection | |
| cell.addEventListener('click', (e) => { | |
| toggleBingoCellSelection(i, cell); | |
| }); | |
| // Add the cell to the grid | |
| gridContainer.appendChild(cell); | |
| } | |
| // Add the grid to the puzzle image container | |
| puzzleImageContainer.appendChild(gridContainer); | |
| // Add a submit button below the grid | |
| const submitSection = document.createElement('div'); | |
| submitSection.className = 'bingo-submit'; | |
| submitSection.style.textAlign = 'center'; | |
| submitSection.style.marginTop = '15px'; | |
| const bingoSubmitBtn = document.createElement('button'); | |
| bingoSubmitBtn.textContent = 'Swap and Submit'; | |
| bingoSubmitBtn.className = 'submit-bingo'; | |
| bingoSubmitBtn.addEventListener('click', () => { | |
| if (bingoSelectedCells.length === 2) { | |
| // Visually swap the cells | |
| swapBingoCells(); | |
| // Submit the answer | |
| setTimeout(submitAnswer, 500); | |
| } else { | |
| resultMessage.textContent = 'Please select exactly two cells to swap.'; | |
| resultMessage.className = 'result-message error'; | |
| } | |
| }); | |
| submitSection.appendChild(bingoSubmitBtn); | |
| // Add to puzzle container | |
| const imageWrapper = document.querySelector('.puzzle-image-wrapper'); | |
| imageWrapper.appendChild(submitSection); | |
| // Reset selected cells | |
| bingoSelectedCells = []; | |
| }; | |
| // Set the source to load the image | |
| fullImg.src = currentPuzzle.image_path; | |
| fullImg.style.display = 'none'; | |
| } | |
| function toggleBingoCellSelection(index, cellElement) { | |
| const overlay = cellElement.querySelector('.cell-overlay'); | |
| // Check if this cell is already selected | |
| const selectedIndex = bingoSelectedCells.indexOf(index); | |
| if (selectedIndex !== -1) { | |
| // If already selected, unselect it | |
| bingoSelectedCells.splice(selectedIndex, 1); | |
| overlay.style.opacity = '0'; | |
| } else { | |
| // If we already have 2 selected cells, remove the first one | |
| if (bingoSelectedCells.length >= 2) { | |
| const firstCell = document.querySelector(`.grid-cell[data-index="${bingoSelectedCells[0]}"]`); | |
| if (firstCell) { | |
| firstCell.querySelector('.cell-overlay').style.opacity = '0'; | |
| } | |
| bingoSelectedCells.shift(); // Remove the first element | |
| } | |
| // Add this cell to selected | |
| bingoSelectedCells.push(index); | |
| overlay.style.opacity = '0.5'; | |
| } | |
| console.log('Selected cells for Bingo:', bingoSelectedCells); | |
| } | |
| function swapBingoCells() { | |
| if (bingoSelectedCells.length !== 2) return; | |
| // Get the two cells to swap | |
| const cell1 = document.querySelector(`.grid-cell[data-index="${bingoSelectedCells[0]}"]`); | |
| const cell2 = document.querySelector(`.grid-cell[data-index="${bingoSelectedCells[1]}"]`); | |
| if (!cell1 || !cell2) return; | |
| // Get the images inside the cells | |
| const img1 = cell1.querySelector('.cell-image'); | |
| const img2 = cell2.querySelector('.cell-image'); | |
| if (!img1 || !img2) return; | |
| // Swap the image sources | |
| const tempSrc = img1.src; | |
| img1.src = img2.src; | |
| img2.src = tempSrc; | |
| // Apply a highlight to the solution line if it exists | |
| if (currentPuzzle.solution_line) { | |
| // Get the answer from the ground truth | |
| const correctSwaps = currentPuzzle.answer; | |
| const selectedSwapSet = new Set(bingoSelectedCells); | |
| // Check which solution was achieved by comparing our selection with possible answers | |
| let solutionKey = null; | |
| // Check vertical solution | |
| if (currentPuzzle.solution_line.vertical && | |
| checkIfSolutionMatches(correctSwaps, selectedSwapSet)) { | |
| solutionKey = 'vertical'; | |
| } | |
| // Check horizontal solution | |
| else if (currentPuzzle.solution_line.horizontal && | |
| checkIfSolutionMatches(correctSwaps, selectedSwapSet)) { | |
| solutionKey = 'horizontal'; | |
| } | |
| // Check diagonal solution | |
| else if (currentPuzzle.solution_line.diagonal && | |
| checkIfSolutionMatches(correctSwaps, selectedSwapSet)) { | |
| solutionKey = 'diagonal'; | |
| } | |
| // If we found a matching solution, highlight it | |
| if (solutionKey && currentPuzzle.solution_line[solutionKey]) { | |
| for (const cellIndex of currentPuzzle.solution_line[solutionKey]) { | |
| const solutionCell = document.querySelector(`.grid-cell[data-index="${cellIndex}"]`); | |
| if (solutionCell) { | |
| solutionCell.style.border = '2px solid green'; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| // Helper function to check if selected cells match any solution | |
| function checkIfSolutionMatches(correctSwaps, selectedSwapSet) { | |
| // Go through each possible correct swap and check if our selection matches any of them | |
| for (const correctSwap of correctSwaps) { | |
| const correctSwapSet = new Set(correctSwap); | |
| // Check if our selected cells match this solution (order doesn't matter) | |
| if (setsEqual(selectedSwapSet, correctSwapSet)) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| // Helper function to compare sets for equality | |
| function setsEqual(set1, set2) { | |
| if (set1.size !== set2.size) return false; | |
| for (const item of set1) { | |
| if (!set2.has(item)) return false; | |
| } | |
| return true; | |
| } | |
| // // Function to set up the debug mode selector | |
| // function setupDebugModeSelector() { | |
| // // Create the debug selector container | |
| // const debugContainer = document.createElement('div'); | |
| // debugContainer.className = 'debug-selector'; | |
| // debugContainer.style.marginTop = '10px'; | |
| // debugContainer.style.marginBottom = '10px'; | |
| // debugContainer.style.padding = '10px'; | |
| // debugContainer.style.backgroundColor = '#f0f0f0'; | |
| // debugContainer.style.borderRadius = '4px'; | |
| // debugContainer.style.display = 'flex'; | |
| // debugContainer.style.alignItems = 'center'; | |
| // debugContainer.style.justifyContent = 'center'; | |
| // debugContainer.style.flexWrap = 'wrap'; | |
| // // Create a label | |
| // const label = document.createElement('label'); | |
| // label.htmlFor = 'debug-type-selector'; | |
| // label.textContent = 'Puzzle Type: '; | |
| // label.style.marginRight = '10px'; | |
| // label.style.fontWeight = 'bold'; | |
| // // Create the select element | |
| // const select = document.createElement('select'); | |
| // select.id = 'debug-type-selector'; | |
| // select.style.padding = '5px'; | |
| // select.style.marginRight = '10px'; | |
| // // Default option - random puzzles | |
| // const defaultOption = document.createElement('option'); | |
| // defaultOption.value = ''; | |
| // defaultOption.textContent = 'Random (All Types)'; | |
| // select.appendChild(defaultOption); | |
| // // Fetch available CAPTCHA types from the API | |
| // fetch('/api/types') | |
| // .then(response => response.json()) | |
| // .then(data => { | |
| // if (data.types && data.types.length > 0) { | |
| // // Add options for each CAPTCHA type | |
| // data.types.forEach(type => { | |
| // const option = document.createElement('option'); | |
| // option.value = type; | |
| // option.textContent = type; | |
| // select.appendChild(option); | |
| // }); | |
| // // Check if there's a debug type in URL parameters | |
| // const urlParams = new URLSearchParams(window.location.search); | |
| // const typeParam = urlParams.get('type'); | |
| // if (typeParam) { | |
| // select.value = typeParam; | |
| // debugPuzzleType = typeParam; | |
| // } | |
| // } | |
| // }) | |
| // .catch(error => { | |
| // console.error('Error fetching CAPTCHA types:', error); | |
| // }); | |
| // // Create apply button | |
| // const applyBtn = document.createElement('button'); | |
| // applyBtn.textContent = 'Apply'; | |
| // applyBtn.style.padding = '5px 10px'; | |
| // applyBtn.style.backgroundColor = '#4CAF50'; | |
| // applyBtn.style.color = 'white'; | |
| // applyBtn.style.border = 'none'; | |
| // applyBtn.style.borderRadius = '4px'; | |
| // applyBtn.style.cursor = 'pointer'; | |
| // // Add event listener to the button | |
| // applyBtn.addEventListener('click', () => { | |
| // debugPuzzleType = select.value; | |
| // // Update URL parameter | |
| // const url = new URL(window.location); | |
| // if (debugPuzzleType) { | |
| // url.searchParams.set('type', debugPuzzleType); | |
| // // Show the debug indicator | |
| // const debugIndicator = document.getElementById('debug-indicator'); | |
| // const debugTypeDisplay = document.getElementById('debug-type-display'); | |
| // if (debugIndicator && debugTypeDisplay) { | |
| // debugTypeDisplay.textContent = debugPuzzleType; | |
| // debugIndicator.style.display = 'block'; | |
| // } | |
| // } else { | |
| // url.searchParams.delete('type'); | |
| // // Hide the debug indicator | |
| // const debugIndicator = document.getElementById('debug-indicator'); | |
| // if (debugIndicator) { | |
| // debugIndicator.style.display = 'none'; | |
| // } | |
| // } | |
| // window.history.pushState({}, '', url); | |
| // // Load a new puzzle with the selected type | |
| // loadNewPuzzle(); | |
| // }); | |
| // // Initialize the debug indicator if there's a type parameter | |
| // if (debugPuzzleType) { | |
| // const debugIndicator = document.getElementById('debug-indicator'); | |
| // const debugTypeDisplay = document.getElementById('debug-type-display'); | |
| // if (debugIndicator && debugTypeDisplay) { | |
| // debugTypeDisplay.textContent = debugPuzzleType; | |
| // debugIndicator.style.display = 'block'; | |
| // } | |
| // } | |
| // // Add elements to container | |
| // debugContainer.appendChild(label); | |
| // debugContainer.appendChild(select); | |
| // debugContainer.appendChild(applyBtn); | |
| // // Add container to the benchmark stats section | |
| // const benchmarkStats = document.querySelector('.benchmark-stats'); | |
| // benchmarkStats.parentNode.insertBefore(debugContainer, benchmarkStats.nextSibling); | |
| // } | |
| // // Function to set up the debug mode selector | |
| function loadNewPuzzle() { | |
| // Reset state | |
| clickCoordinates = null; | |
| processingClick = false; | |
| currentRotationAngle = 0; | |
| selectedCells = []; | |
| bingoSelectedCells = []; | |
| // Remove any click markers and debug areas | |
| const existingMarker = document.querySelector('.click-marker'); | |
| if (existingMarker) { | |
| existingMarker.remove(); | |
| } | |
| const existingArea = document.querySelector('.debug-area'); | |
| if (existingArea) { | |
| existingArea.remove(); | |
| } | |
| // Remove any rotation controls | |
| const existingControls = document.querySelector('.rotation-controls'); | |
| if (existingControls) { | |
| existingControls.remove(); | |
| } | |
| // Remove any existing rotation submit buttons | |
| const existingRotationSubmit = document.querySelector('.rotation-submit'); | |
| if (existingRotationSubmit) { | |
| existingRotationSubmit.remove(); | |
| } | |
| // Remove any slider components and submit buttons | |
| const existingSliderSubmit = document.querySelector('.slider-submit'); | |
| if (existingSliderSubmit) { | |
| existingSliderSubmit.remove(); | |
| } | |
| // Remove any unusual detection grid and submit buttons | |
| const existingUnusualSubmit = document.querySelector('.unusual-submit'); | |
| if (existingUnusualSubmit) { | |
| existingUnusualSubmit.remove(); | |
| } | |
| // Remove any image recognition grid and submit buttons | |
| const existingImageRecognitionSubmit = document.querySelector('.image-recognition-submit'); | |
| if (existingImageRecognitionSubmit) { | |
| existingImageRecognitionSubmit.remove(); | |
| } | |
| // Remove any bingo grid and submit buttons | |
| const existingBingoSubmit = document.querySelector('.bingo-submit'); | |
| if (existingBingoSubmit) { | |
| existingBingoSubmit.remove(); | |
| } | |
| // After checking and removing existingImageMatchingControls | |
| const existingImageMatchingControls = document.querySelector('.image-matching-controls'); | |
| if (existingImageMatchingControls) { | |
| existingImageMatchingControls.remove(); | |
| } | |
| const existingImageMatchingSubmit = document.querySelector('.image-matching-submit'); | |
| if (existingImageMatchingSubmit) { | |
| existingImageMatchingSubmit.remove(); | |
| } | |
| // Remove any dart count controls and submit buttons | |
| const existingDartCountSubmit = document.querySelector('.dart-count-submit'); | |
| if (existingDartCountSubmit) { | |
| existingDartCountSubmit.remove(); | |
| } | |
| // Remove any object match controls and submit buttons | |
| const existingObjectMatchSubmit = document.querySelector('.object-match-submit'); | |
| if (existingObjectMatchSubmit) { | |
| existingObjectMatchSubmit.remove(); | |
| } | |
| // Remove any connect icon controls and submit buttons | |
| const existingConnectIconSubmit = document.querySelector('.connect-icon-submit'); | |
| if (existingConnectIconSubmit) { | |
| existingConnectIconSubmit.remove(); | |
| } | |
| // Remove any hold button components | |
| const existingHoldButton = document.querySelector('.hold-button-container'); | |
| if (existingHoldButton) { | |
| existingHoldButton.remove(); | |
| } | |
| // Reset the puzzle prompt and image | |
| puzzlePrompt.textContent = 'Loading puzzle...'; | |
| resultMessage.textContent = ''; | |
| resultMessage.className = 'result-message'; | |
| // Reset the submit button text | |
| submitBtn.textContent = 'Submit'; | |
| submitBtn.disabled = false; | |
| // Reset input field display | |
| userAnswerInput.style.display = 'block'; | |
| // Construct URL with debug type parameter if set | |
| let url = '/api/get_puzzle?mode=sequential'; | |
| // // Function to set up the debug mode selector | |
| // if (debugPuzzleType) { | |
| // url = `/api/get_puzzle?debug_type=${encodeURIComponent(debugPuzzleType)}`; | |
| // } | |
| // Get a random puzzle from any available type | |
| fetch(url) | |
| .then(response => response.json()) | |
| .then(data => { | |
| console.log("Received puzzle data:", data); | |
| currentPuzzle = data; | |
| // Update the puzzle prompt | |
| if (data.prompt) { | |
| puzzlePrompt.textContent = data.prompt; | |
| } else if (data.puzzle_type === 'Dice_Count') { | |
| puzzlePrompt.textContent = "Sum up the numbers on all the dice"; | |
| } | |
| // Important: Always display difficulty stars based on puzzle type | |
| displayDifficultyStars(data.puzzle_type); | |
| // Reset container | |
| puzzleImageContainer.innerHTML = ''; | |
| // Configure input based on puzzle type | |
| if (data.input_type === 'click') { | |
| // Setup for click-based CAPTCHAs (Geometry_Click, Misleading_Click, Pick_Area) | |
| puzzleImage.src = data.image_path; | |
| inputGroup.style.display = 'none'; | |
| puzzleImage.style.cursor = 'pointer'; | |
| puzzleImage.classList.add('clickable'); | |
| // Add puzzle image back to container | |
| if (puzzleImageContainer.innerHTML === '') { | |
| puzzleImageContainer.appendChild(puzzleImage); | |
| } | |
| puzzleImageContainer.style.display = 'block'; | |
| puzzleImage.style.display = 'block'; | |
| // Reset click coordinates for new puzzle | |
| clickCoordinates = null; | |
| // Update prompt text | |
| if (data.prompt) { | |
| puzzlePrompt.textContent = data.prompt; | |
| } else if (data.puzzle_type === 'Geometry_Click') { | |
| puzzlePrompt.textContent = "Click on the geometric shape"; | |
| } else if (data.puzzle_type === 'Misleading_Click') { | |
| puzzlePrompt.textContent = "Click the image to continue"; | |
| // Make sure avoid_area is stored in currentPuzzle object | |
| if (data.avoid_area) { | |
| currentPuzzle.avoid_area = data.avoid_area; | |
| console.log('Loaded avoid_area:', data.avoid_area); | |
| } | |
| } else if (data.puzzle_type === 'Pick_Area') { | |
| puzzlePrompt.textContent = "Click on the largest area outlined by the dotted line"; | |
| } | |
| // For debugging, when image loads, show the target areas | |
| puzzleImage.onload = () => { | |
| if (DEBUG_MODE) { | |
| // Show ground truth area differently based on puzzle type | |
| if (data.puzzle_type === 'Pick_Area') { | |
| showPickAreaTargets(puzzleImageContainer); | |
| } else if (data.puzzle_type === 'Geometry_Click') { | |
| fetchAndShowGeometryClickArea(puzzleImageContainer); | |
| } else if (data.puzzle_type === 'Misleading_Click') { | |
| // For misleading click, show the area to avoid | |
| if (data.avoid_area) { | |
| showMisleadingClickArea(puzzleImageContainer, data.avoid_area); | |
| } | |
| } | |
| } | |
| }; | |
| } else if (data.input_type === 'rotation') { | |
| // Setup for rotation-based CAPTCHAs | |
| inputGroup.style.display = 'none'; | |
| puzzleImage.style.display = 'none'; | |
| puzzleImageContainer.style.display = 'block'; | |
| // Update prompt first to ensure it's from the rotation puzzle | |
| if (data.prompt) { | |
| puzzlePrompt.textContent = data.prompt; | |
| } else { | |
| puzzlePrompt.textContent = "Use the arrows to rotate the object to match the reference direction."; | |
| } | |
| // Set up rotation interface | |
| setupRotationControls(); | |
| // Auto-show submit button for rotation puzzles | |
| const submitSection = document.createElement('div'); | |
| submitSection.className = 'rotation-submit'; | |
| const rotateSubmitBtn = document.createElement('button'); | |
| rotateSubmitBtn.textContent = 'Submit'; | |
| rotateSubmitBtn.className = 'submit-rotation'; | |
| rotateSubmitBtn.addEventListener('click', submitAnswer); | |
| submitSection.appendChild(rotateSubmitBtn); | |
| // Add to puzzle container | |
| const imageWrapper = document.querySelector('.puzzle-image-wrapper'); | |
| imageWrapper.appendChild(submitSection); | |
| } else if (data.input_type === 'slide') { | |
| // Setup for slide-based CAPTCHAs | |
| inputGroup.style.display = 'none'; | |
| puzzleImage.style.display = 'none'; | |
| puzzleImageContainer.style.display = 'block'; | |
| // Update prompt for the slide puzzle | |
| if (data.prompt) { | |
| puzzlePrompt.textContent = data.prompt; | |
| } else { | |
| puzzlePrompt.textContent = "Drag the slider component to the correct position."; | |
| } | |
| // Set up sliding puzzle interface | |
| setupSlidePuzzle(); | |
| } else if (data.input_type === 'multiselect') { | |
| // Setup for unusual detection CAPTCHAs | |
| inputGroup.style.display = 'none'; | |
| puzzleImage.style.display = 'none'; | |
| puzzleImageContainer.style.display = 'block'; | |
| // Update prompt for the unusual detection puzzle | |
| if (data.prompt) { | |
| puzzlePrompt.textContent = data.prompt; | |
| } else { | |
| puzzlePrompt.textContent = "Select the unusual items in the image."; | |
| } | |
| // Set up unusual detection grid | |
| setupUnusualDetectionGrid(); | |
| } else if (data.input_type === 'image_grid') { | |
| // Setup for image recognition CAPTCHAs | |
| inputGroup.style.display = 'none'; | |
| puzzleImage.style.display = 'none'; | |
| puzzleImageContainer.style.display = 'block'; | |
| // Update prompt for the image recognition puzzle | |
| if (data.prompt) { | |
| puzzlePrompt.textContent = data.prompt; | |
| } else if (data.question) { | |
| puzzlePrompt.textContent = data.question; | |
| } else { | |
| puzzlePrompt.textContent = "Select all images that match the description."; | |
| } | |
| // Set up image recognition grid | |
| setupImageRecognition(); | |
| } else if (data.input_type === 'bingo_swap') { | |
| // Setup for Bingo swap CAPTCHA | |
| inputGroup.style.display = 'none'; | |
| puzzleImage.style.display = 'none'; | |
| puzzleImageContainer.style.display = 'block'; | |
| // Update prompt for the Bingo puzzle | |
| if (data.prompt) { | |
| puzzlePrompt.textContent = data.prompt; | |
| } else { | |
| puzzlePrompt.textContent = "Please click two images to exchange their position to line up the same images to a line, you can only exchange the images once."; | |
| } | |
| // Set up Bingo grid | |
| setupBingoSwap(); | |
| } else if (data.input_type === 'image_matching') { | |
| // Setup for Image Matching CAPTCHA | |
| inputGroup.style.display = 'none'; | |
| puzzleImage.style.display = 'none'; | |
| puzzleImageContainer.style.display = 'block'; | |
| // Update prompt for the Image Matching puzzle | |
| if (data.prompt) { | |
| puzzlePrompt.textContent = data.prompt; | |
| } else { | |
| puzzlePrompt.textContent = "Using the arrows, match the animal in the left and right image."; | |
| } | |
| // Set up Image Matching interface | |
| setupImageMatching(); | |
| } else if (data.input_type === 'patch_select') { | |
| // Hide standard input display but keep it for value storage | |
| userAnswerInput.style.display = 'none'; | |
| // Customize submit button | |
| submitBtn.textContent = 'Verify'; | |
| submitBtn.style.display = 'block'; | |
| // Setup patch selection grid | |
| setupPatchSelectGrid(); | |
| } else if (data.input_type === 'dart_count') { | |
| // Hide standard input display but keep it for value storage | |
| userAnswerInput.style.display = 'none'; | |
| inputGroup.style.display = 'none'; | |
| puzzleImage.style.display = 'none'; | |
| puzzleImageContainer.style.display = 'block'; | |
| // Update prompt for the dart count puzzle | |
| if (data.prompt) { | |
| puzzlePrompt.textContent = data.prompt; | |
| } else { | |
| puzzlePrompt.textContent = "Use the arrows to find the darts that add up to the target number."; | |
| } | |
| // Debug log | |
| console.log('Setting up Dart Count puzzle with data:', data); | |
| // Setup dart count interface | |
| setupDartCount(); | |
| } else if (data.input_type === 'select_animal') { | |
| // Hide standard input display but keep it for value storage | |
| userAnswerInput.style.display = 'none'; | |
| // Customize submit button | |
| submitBtn.textContent = 'Submit'; | |
| submitBtn.style.display = 'block'; | |
| // Setup animal selection grid | |
| setupSelectAnimalGrid(); | |
| } else if (data.input_type === 'object_match') { | |
| // Setup for object match puzzles | |
| inputGroup.style.display = 'none'; | |
| puzzleImage.style.display = 'none'; | |
| puzzleImageContainer.style.display = 'block'; | |
| // Update prompt | |
| if (data.prompt) { | |
| puzzlePrompt.textContent = data.prompt; | |
| } else { | |
| puzzlePrompt.textContent = "Use the arrows to change the number of objects until it matches the left image."; | |
| } | |
| // Set up object match interface | |
| setupObjectMatch(); | |
| } else if (data.input_type === 'place_dot') { | |
| // Setup for Place_Dot CAPTCHAs | |
| inputGroup.style.display = 'none'; | |
| puzzleImage.style.display = 'none'; | |
| puzzleImageContainer.style.display = 'block'; | |
| // Update prompt | |
| if (data.prompt) { | |
| puzzlePrompt.textContent = data.prompt; | |
| } else { | |
| puzzlePrompt.textContent = "Click to place a Dot at the end of the car's path"; | |
| } | |
| // Set up place dot interface | |
| setupPlaceDot(); | |
| } else if (data.input_type === 'connect_icon') { | |
| // Setup for Connect_icon CAPTCHAs | |
| inputGroup.style.display = 'none'; | |
| puzzleImage.style.display = 'none'; | |
| puzzleImageContainer.style.display = 'block'; | |
| // Update prompt | |
| if (data.prompt) { | |
| puzzlePrompt.textContent = data.prompt; | |
| } else { | |
| puzzlePrompt.textContent = "Using the arrows, connect the same two icons with the dotted line as shown on the left."; | |
| } | |
| // Set up connect icon interface | |
| setupConnectIcon(); | |
| } else if (data.input_type === 'click_order') { | |
| // Setup for Click_Order CAPTCHAs | |
| inputGroup.style.display = 'none'; | |
| puzzleImage.style.display = 'none'; | |
| puzzleImageContainer.style.display = 'block'; | |
| // Update prompt | |
| if (data.prompt) { | |
| puzzlePrompt.textContent = data.prompt; | |
| } else { | |
| puzzlePrompt.textContent = "Click the icons in order as shown in the reference image."; | |
| } | |
| // Set up click order interface | |
| setupClickOrder(); | |
| } else if (data.input_type === 'hold_button') { | |
| // Setup for Hold_Button CAPTCHAs | |
| inputGroup.style.display = 'flex'; | |
| puzzleImage.style.display = 'none'; | |
| puzzleImageContainer.style.display = 'block'; | |
| // Update prompt | |
| if (data.prompt) { | |
| puzzlePrompt.textContent = data.prompt; | |
| } else { | |
| puzzlePrompt.textContent = "Hold the button until it finishes loading."; | |
| } | |
| // Set up hold button interface | |
| setupHoldButton(); | |
| // Ensure input field and submit button are visible | |
| userAnswerInput.style.display = 'block'; | |
| submitBtn.style.display = 'inline-block'; | |
| } else { | |
| // Default for text-based CAPTCHAs | |
| puzzleImage.src = data.image_path; | |
| inputGroup.style.display = 'flex'; | |
| puzzleImage.style.cursor = 'default'; | |
| puzzleImage.classList.remove('clickable'); | |
| // Add puzzle image back to container | |
| if (puzzleImageContainer.innerHTML === '') { | |
| puzzleImageContainer.appendChild(puzzleImage); | |
| } | |
| puzzleImageContainer.style.display = 'block'; | |
| puzzleImage.style.display = 'block'; | |
| // Update prompt after clearing | |
| if (data.prompt) { | |
| puzzlePrompt.textContent = data.prompt; | |
| } else if (data.puzzle_type === 'Dice_Count') { | |
| puzzlePrompt.textContent = "Sum up the numbers on all the dice"; | |
| } | |
| // Reset submit button | |
| submitBtn.disabled = false; | |
| submitBtn.textContent = 'Submit'; | |
| // Clear and focus input | |
| userAnswerInput.value = ''; | |
| userAnswerInput.focus(); | |
| // Set input type based on puzzle type | |
| if (data.input_type === 'number') { | |
| userAnswerInput.setAttribute('type', 'number'); | |
| userAnswerInput.setAttribute('placeholder', 'Enter the sum'); | |
| } else { | |
| userAnswerInput.setAttribute('type', 'text'); | |
| userAnswerInput.setAttribute('placeholder', 'Your answer'); | |
| } | |
| // Ensure the input is visible | |
| userAnswerInput.style.display = 'block'; | |
| } | |
| }) | |
| .catch(error => { | |
| console.error('Error loading puzzle:', error); | |
| // Try again after a delay if there was an error | |
| setTimeout(loadNewPuzzle, 3000); | |
| }); | |
| } | |
| // Function to create fireworks effect for correct answers | |
| function createFireworks() { | |
| // Create container for fireworks | |
| const fireworksContainer = document.createElement('div'); | |
| fireworksContainer.className = 'fireworks-container'; | |
| document.body.appendChild(fireworksContainer); | |
| // Create happy face animation | |
| const happyFaceContainer = document.createElement('div'); | |
| happyFaceContainer.className = 'happy-face-container'; | |
| happyFaceContainer.textContent = '😄'; | |
| happyFaceContainer.style.zIndex = '10000'; // Ensure it's above everything | |
| document.body.appendChild(happyFaceContainer); | |
| // Create multiple fireworks at random positions | |
| const colors = [ | |
| '#FF0000', '#00FF00', '#0000FF', '#FFFF00', | |
| '#FF00FF', '#00FFFF', '#FFA500', '#FF4500', | |
| '#FFD700', '#32CD32', '#8A2BE2', '#FF69B4' | |
| ]; | |
| // Create more fireworks (150 instead of 100) | |
| for (let i = 0; i < 150; i++) { | |
| const firework = document.createElement('div'); | |
| firework.className = 'firework'; | |
| // Random position - spread across the screen, with more concentration near center | |
| const centerBias = Math.random() > 0.7; // 30% chance to be centered | |
| const x = centerBias | |
| ? window.innerWidth/2 + (Math.random() - 0.5) * window.innerWidth/2 | |
| : Math.random() * window.innerWidth; | |
| const y = centerBias | |
| ? window.innerHeight/2 + (Math.random() - 0.5) * window.innerHeight/2 | |
| : Math.random() * window.innerHeight; | |
| // Random color | |
| const color = colors[Math.floor(Math.random() * colors.length)]; | |
| // Random size (larger particles) | |
| const size = 5 + Math.random() * 8; | |
| // Random delay and duration | |
| const delay = Math.random() * 1.5; | |
| const duration = 0.8 + Math.random() * 1.2; | |
| // Apply styles | |
| firework.style.left = `${x}px`; | |
| firework.style.top = `${y}px`; | |
| firework.style.backgroundColor = color; | |
| firework.style.width = `${size}px`; | |
| firework.style.height = `${size}px`; | |
| firework.style.animationDelay = `${delay}s`; | |
| firework.style.animationDuration = `${duration}s`; | |
| // Add to container | |
| fireworksContainer.appendChild(firework); | |
| } | |
| // Remove containers after animation completes | |
| setTimeout(() => { | |
| fireworksContainer.remove(); | |
| happyFaceContainer.remove(); | |
| }, 3500); | |
| } | |
| // Function to create sad face effect for incorrect answers | |
| function createSadFace() { | |
| // Create container for sad face | |
| const sadFaceContainer = document.createElement('div'); | |
| sadFaceContainer.className = 'sad-face-container'; | |
| sadFaceContainer.textContent = '😢'; | |
| document.body.appendChild(sadFaceContainer); | |
| // Remove container after animation completes | |
| setTimeout(() => { | |
| sadFaceContainer.remove(); | |
| }, 2000); | |
| } | |
| function submitAnswer() { | |
| // Don't submit if there's no input for number/text input types | |
| if ((currentPuzzle.input_type === 'number' || currentPuzzle.input_type === 'text') && | |
| !userAnswerInput.value.trim()) { | |
| // Don't submit empty answers for number/text inputs | |
| return; | |
| } | |
| // Disable submit button to prevent double submissions | |
| submitBtn.disabled = true; | |
| submitBtn.textContent = 'Processing...'; | |
| let answerData = { | |
| puzzle_type: currentPuzzle.puzzle_type, | |
| puzzle_id: currentPuzzle.puzzle_id | |
| }; | |
| // Handle different input types | |
| if (currentPuzzle.input_type === 'click' && clickCoordinates) { | |
| // For click input, send the click coordinates | |
| answerData.answer = clickCoordinates; | |
| } else if (currentPuzzle.input_type === 'rotation') { | |
| // For rotation input, send the current rotation angle | |
| answerData.answer = currentRotationAngle; | |
| } else if (currentPuzzle.input_type === 'slide') { | |
| // For slide puzzle, calculate the current position of the slider | |
| const sliderComponent = document.querySelector('.slider-component'); | |
| if (sliderComponent) { | |
| // Get the current position (from CSS left/top values) | |
| const currentX = parseInt(sliderComponent.style.left) || 0; | |
| const currentY = parseInt(sliderComponent.style.top) || 0; | |
| // Add slider position to answer data | |
| answerData.answer = [currentX, currentY]; | |
| } else { | |
| console.error('Slider component not found'); | |
| // Re-enable submit button | |
| submitBtn.disabled = false; | |
| submitBtn.textContent = 'Check Position'; | |
| return; | |
| } | |
| } else if (currentPuzzle.input_type === 'multiselect') { | |
| // For multiselect input, send the selected cell indices | |
| answerData.answer = selectedCells; | |
| } else if (currentPuzzle.input_type === 'image_grid') { | |
| // For image grid selection, send the selected image indices | |
| answerData.answer = selectedCells; | |
| } else if (currentPuzzle.input_type === 'bingo_swap') { | |
| // For bingo swap, send the selected cells to swap | |
| answerData.answer = bingoSelectedCells; | |
| } else if (currentPuzzle.input_type === 'image_matching') { | |
| // For image matching, send the current option index | |
| const currentOptionIndex = currentPuzzle.current_option_index || 0; | |
| answerData.answer = currentOptionIndex; | |
| } else if (currentPuzzle.input_type === 'dart_count') { | |
| // For dart count, send the selected option index | |
| const selectedIndex = parseInt(userAnswerInput.value); | |
| answerData.answer = selectedIndex; | |
| } else if (currentPuzzle.input_type === 'patch_select') { | |
| // For patch select, send the selected patch indices | |
| try { | |
| // Try to parse the JSON value from the input | |
| const parsedSelection = JSON.parse(userAnswerInput.value); | |
| // If parsed array is empty but global selectedCells is not, use global | |
| if (parsedSelection.length === 0 && selectedCells.length > 0) { | |
| answerData.answer = selectedCells; | |
| } else { | |
| answerData.answer = parsedSelection; | |
| } | |
| } catch (error) { | |
| console.error('Error parsing selected patches:', error); | |
| // Fallback to the global array if parsing fails | |
| answerData.answer = selectedCells; | |
| } | |
| } else if (currentPuzzle.input_type === 'select_animal') { | |
| // For select animal, send the selected animal index | |
| try { | |
| // If the value is empty, use the global selectedAnimalIndex | |
| if (userAnswerInput.value === '[]' || userAnswerInput.value.trim() === '') { | |
| answerData.answer = selectedAnimalIndex >= 0 ? [selectedAnimalIndex] : []; | |
| } else { | |
| // Otherwise parse the JSON from the input | |
| const selectedAnimal = JSON.parse(userAnswerInput.value); | |
| answerData.answer = selectedAnimal; | |
| } | |
| } catch (error) { | |
| console.error('Error parsing selected animal:', error); | |
| // Use the global variable as a fallback | |
| answerData.answer = selectedAnimalIndex >= 0 ? [selectedAnimalIndex] : []; | |
| } | |
| } else if (currentPuzzle.input_type === 'object_match') { | |
| // For object match, send the selected option index | |
| const selectedIndex = parseInt(puzzleImageContainer.dataset.currentOptionIndex); | |
| answerData.answer = selectedIndex; | |
| } else if (currentPuzzle.input_type === 'place_dot') { | |
| // For place_dot input, send the click coordinates | |
| if (!clickCoordinates) { | |
| console.error('No dot coordinates found'); | |
| // Re-enable submit button | |
| submitBtn.disabled = false; | |
| submitBtn.textContent = 'Submit'; | |
| return; | |
| } | |
| answerData.answer = clickCoordinates; | |
| } else if (currentPuzzle.input_type === 'connect_icon') { | |
| // For connect_icon, send the current option index | |
| answerData.answer = parseInt(userAnswerInput.value) || 0; | |
| } else if (currentPuzzle.input_type === 'hold_button') { | |
| // For hold button, get the elapsed time from the input field | |
| answerData.answer = parseFloat(userAnswerInput.value) || 0; | |
| answerData.elapsed_time = ((Date.now() - puzzleStartTime) / 1000).toFixed(2); | |
| } else { | |
| // For text/number inputs, use the input value | |
| answerData.answer = userAnswerInput.value.trim(); | |
| } | |
| // Send answer to server for verification | |
| fetch('/api/check_answer', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify(answerData) | |
| }) | |
| .then(response => response.json()) | |
| .then(data => { | |
| // Update stats | |
| benchmarkStats.total++; | |
| if (data.correct) { | |
| benchmarkStats.correct++; | |
| resultMessage.textContent = 'Correct!'; | |
| resultMessage.className = 'result-message correct'; | |
| // Create fireworks effect for correct answer | |
| createFireworks(); | |
| } else { | |
| // Just show "Incorrect" without revealing the correct answer | |
| resultMessage.textContent = 'Incorrect.'; | |
| resultMessage.className = 'result-message incorrect'; | |
| // Create sad face effect for incorrect answer | |
| createSadFace(); | |
| } | |
| updateStats(); | |
| // Record benchmark result | |
| recordBenchmarkResult({ | |
| puzzle_type: currentPuzzle.puzzle_type, | |
| puzzle_id: currentPuzzle.puzzle_id, | |
| user_answer: answerData.answer, | |
| correct_answer: data.correct_answer, | |
| correct: data.correct | |
| }); | |
| // Disable the submit button after submission | |
| if (currentPuzzle.input_type !== 'click') { | |
| submitBtn.disabled = true; | |
| // Also disable rotation submit button if it exists | |
| const rotateSubmitBtn = document.querySelector('.submit-rotation'); | |
| if (rotateSubmitBtn) { | |
| rotateSubmitBtn.disabled = true; | |
| } | |
| // Also disable image recognition submit button if it exists | |
| const imageRecognitionSubmitBtn = document.querySelector('.submit-image-recognition'); | |
| if (imageRecognitionSubmitBtn) { | |
| imageRecognitionSubmitBtn.disabled = true; | |
| } | |
| // Also disable bingo submit button if it exists | |
| const bingoSubmitBtn = document.querySelector('.submit-bingo'); | |
| if (bingoSubmitBtn) { | |
| bingoSubmitBtn.disabled = true; | |
| } | |
| // Also disable image matching submit button if it exists | |
| const imageMatchingSubmitBtn = document.querySelector('.submit-image-matching'); | |
| if (imageMatchingSubmitBtn) { | |
| imageMatchingSubmitBtn.disabled = true; | |
| } | |
| } | |
| // After handling the result and before loading a new puzzle | |
| setTimeout(() => { | |
| // Reset the submit button text before loading new puzzle | |
| submitBtn.textContent = 'Submit'; | |
| // Make sure we reset input visibility before loading a new puzzle | |
| userAnswerInput.style.display = 'block'; | |
| loadNewPuzzle(); | |
| }, 2000); | |
| }) | |
| .catch(error => { | |
| console.error('Error checking answer:', error); | |
| resultMessage.textContent = 'Error checking answer. Please try again.'; | |
| resultMessage.className = 'result-message incorrect'; | |
| // Re-enable the submit button on error | |
| submitBtn.disabled = false; | |
| submitBtn.textContent = 'Submit'; | |
| }); | |
| } | |
| function updateStats() { | |
| totalCount.textContent = benchmarkStats.total; | |
| correctCount.textContent = benchmarkStats.correct; | |
| const accuracy = benchmarkStats.total > 0 | |
| ? ((benchmarkStats.correct / benchmarkStats.total) * 100).toFixed(1) | |
| : '0.0'; | |
| accuracyEl.textContent = `${accuracy}%`; | |
| } | |
| function recordBenchmarkResult(result) { | |
| // Ensure we have the timestamp field | |
| if (!result.timestamp) { | |
| result.timestamp = new Date().toISOString(); | |
| } | |
| // Send the benchmark result to be recorded | |
| fetch('/api/benchmark_results', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify(result) | |
| }) | |
| .then(response => response.json()) | |
| .then(data => { | |
| console.log('Benchmark result recorded:', data); | |
| }) | |
| .catch(error => { | |
| console.error('Error recording benchmark result:', error); | |
| }); | |
| } | |
| // Auto-start benchmark when page loads | |
| loadNewPuzzle(); | |
| // Function to update position display for the slider | |
| function updateSliderPositionDisplay(x, y, componentWidth, componentHeight) { | |
| // Remove any existing position display | |
| const existingDisplay = document.querySelector('.slider-position-display'); | |
| if (existingDisplay) { | |
| existingDisplay.remove(); | |
| } | |
| if (!DEBUG_MODE) return; | |
| // Calculate center point | |
| const centerX = x + (componentWidth / 2); | |
| const centerY = y + (componentHeight / 2); | |
| // Create the position display element | |
| const posDisplay = document.createElement('div'); | |
| posDisplay.className = 'slider-position-display'; | |
| posDisplay.textContent = `Position: (${Math.round(centerX)}, ${Math.round(centerY)})`; | |
| posDisplay.style.position = 'fixed'; | |
| posDisplay.style.top = '10px'; | |
| posDisplay.style.right = '10px'; | |
| posDisplay.style.backgroundColor = 'rgba(0, 0, 0, 0.7)'; | |
| posDisplay.style.color = 'white'; | |
| posDisplay.style.padding = '5px 10px'; | |
| posDisplay.style.borderRadius = '4px'; | |
| posDisplay.style.fontSize = '12px'; | |
| posDisplay.style.zIndex = '1000'; | |
| // Add to document body | |
| document.body.appendChild(posDisplay); | |
| } | |
| // Function to set up image recognition grid | |
| function setupImageRecognition() { | |
| // Remove any existing grid | |
| const existingGrid = document.querySelector('.image-recognition-grid'); | |
| if (existingGrid) { | |
| existingGrid.remove(); | |
| } | |
| // Clear the puzzle image container | |
| puzzleImageContainer.innerHTML = ''; | |
| // Get the grid dimensions | |
| const gridSize = currentPuzzle.grid_size || [3, 3]; // Default to 3x3 grid | |
| const [rows, cols] = gridSize; | |
| // Create the grid container | |
| const gridContainer = document.createElement('div'); | |
| gridContainer.className = 'image-recognition-grid'; | |
| gridContainer.style.display = 'grid'; | |
| gridContainer.style.gridTemplateColumns = `repeat(${cols}, 1fr)`; | |
| gridContainer.style.gridTemplateRows = `repeat(${rows}, 1fr)`; | |
| gridContainer.style.gap = '5px'; | |
| gridContainer.style.width = '100%'; | |
| gridContainer.style.aspectRatio = `${cols} / ${rows}`; | |
| // Get the list of images | |
| const images = currentPuzzle.images || []; | |
| // Create individual cells for each image | |
| for (let i = 0; i < images.length; i++) { | |
| const cell = document.createElement('div'); | |
| cell.className = 'grid-cell'; | |
| cell.dataset.index = i; | |
| cell.style.position = 'relative'; | |
| cell.style.border = '2px solid #333'; | |
| cell.style.cursor = 'pointer'; | |
| cell.style.overflow = 'hidden'; | |
| // Create image element | |
| const img = document.createElement('img'); | |
| img.src = images[i]; | |
| img.style.width = '100%'; | |
| img.style.height = '100%'; | |
| img.style.objectFit = 'cover'; | |
| img.style.display = 'block'; | |
| cell.appendChild(img); | |
| // Create an overlay for selection state | |
| const overlay = document.createElement('div'); | |
| overlay.className = 'cell-overlay'; | |
| overlay.style.position = 'absolute'; | |
| overlay.style.top = '0'; | |
| overlay.style.left = '0'; | |
| overlay.style.width = '100%'; | |
| overlay.style.height = '100%'; | |
| overlay.style.backgroundColor = 'rgba(0, 120, 255, 0.5)'; | |
| overlay.style.opacity = '0'; | |
| overlay.style.transition = 'opacity 0.2s ease'; | |
| overlay.style.pointerEvents = 'none'; | |
| cell.appendChild(overlay); | |
| // Add a checkmark icon to indicate selection | |
| const checkmark = document.createElement('div'); | |
| checkmark.className = 'checkmark'; | |
| checkmark.innerHTML = '✓'; | |
| checkmark.style.position = 'absolute'; | |
| checkmark.style.top = '50%'; | |
| checkmark.style.left = '50%'; | |
| checkmark.style.transform = 'translate(-50%, -50%)'; | |
| checkmark.style.color = 'white'; | |
| checkmark.style.fontSize = '32px'; | |
| checkmark.style.fontWeight = 'bold'; | |
| checkmark.style.opacity = '0'; | |
| checkmark.style.transition = 'opacity 0.2s ease'; | |
| checkmark.style.pointerEvents = 'none'; | |
| cell.appendChild(checkmark); | |
| // Add click handler for selection | |
| cell.addEventListener('click', (e) => { | |
| toggleCellSelection(i, cell); | |
| }); | |
| // Add the cell to the grid | |
| gridContainer.appendChild(cell); | |
| } | |
| // Add the grid to the puzzle image container | |
| puzzleImageContainer.appendChild(gridContainer); | |
| // Add a submit button below the grid | |
| const submitSection = document.createElement('div'); | |
| submitSection.className = 'image-recognition-submit'; | |
| submitSection.style.textAlign = 'center'; | |
| submitSection.style.marginTop = '15px'; | |
| const submitBtn = document.createElement('button'); | |
| submitBtn.textContent = 'Submit'; | |
| submitBtn.className = 'submit-image-recognition'; | |
| submitBtn.addEventListener('click', submitAnswer); | |
| submitSection.appendChild(submitBtn); | |
| // Add to puzzle container | |
| const imageWrapper = document.querySelector('.puzzle-image-wrapper'); | |
| imageWrapper.appendChild(submitSection); | |
| // Reset selected cells | |
| selectedCells = []; | |
| } | |
| // Function to set up Image Matching puzzle | |
| function setupImageMatching() { | |
| // Remove any existing controls first | |
| const existingControls = document.querySelector('.image-matching-controls'); | |
| if (existingControls) { | |
| existingControls.remove(); | |
| } | |
| // Clear the puzzle image container | |
| puzzleImageContainer.innerHTML = ''; | |
| // Create a container for the reference image | |
| const referenceContainer = document.createElement('div'); | |
| referenceContainer.className = 'reference-image-container'; | |
| const referenceImg = document.createElement('img'); | |
| referenceImg.id = 'reference-image'; | |
| referenceImg.src = currentPuzzle.reference_image; | |
| referenceImg.alt = 'Reference image'; | |
| referenceContainer.appendChild(referenceImg); | |
| // Create a container for the option image | |
| const optionContainer = document.createElement('div'); | |
| optionContainer.className = 'option-image-container'; | |
| const optionImg = document.createElement('img'); | |
| optionImg.id = 'option-image'; | |
| optionImg.src = currentPuzzle.option_images[0]; // Start with the first option | |
| optionImg.alt = 'Option image'; | |
| optionContainer.appendChild(optionImg); | |
| // Create a two-column layout for image matching puzzle | |
| const matchingLayout = document.createElement('div'); | |
| matchingLayout.className = 'matching-layout'; | |
| matchingLayout.appendChild(referenceContainer); | |
| matchingLayout.appendChild(optionContainer); | |
| // Replace the existing puzzle image | |
| puzzleImageContainer.innerHTML = ''; | |
| puzzleImageContainer.appendChild(matchingLayout); | |
| // Create navigation controls | |
| const navControls = document.createElement('div'); | |
| navControls.className = 'image-matching-controls'; | |
| // Create left navigation button | |
| const leftBtn = document.createElement('button'); | |
| leftBtn.className = 'navigate-left'; | |
| leftBtn.innerHTML = '◀'; // Left arrow | |
| leftBtn.setAttribute('aria-label', 'Previous image'); | |
| // Create right navigation button | |
| const rightBtn = document.createElement('button'); | |
| rightBtn.className = 'navigate-right'; | |
| rightBtn.innerHTML = '▶'; // Right arrow | |
| rightBtn.setAttribute('aria-label', 'Next image'); | |
| // Create indicator dots | |
| const indicatorContainer = document.createElement('div'); | |
| indicatorContainer.className = 'indicator-dots'; | |
| for (let i = 0; i < currentPuzzle.option_images.length; i++) { | |
| const dot = document.createElement('span'); | |
| dot.className = i === 0 ? 'dot active' : 'dot'; | |
| indicatorContainer.appendChild(dot); | |
| } | |
| // Add buttons and indicators to controls | |
| navControls.appendChild(leftBtn); | |
| navControls.appendChild(indicatorContainer); | |
| navControls.appendChild(rightBtn); | |
| // Add to puzzle container | |
| const imageWrapper = document.querySelector('.puzzle-image-wrapper'); | |
| imageWrapper.appendChild(navControls); | |
| // Add event listeners for navigation buttons | |
| let currentIndex = 0; | |
| leftBtn.addEventListener('click', () => { | |
| currentIndex = (currentIndex - 1 + currentPuzzle.option_images.length) % currentPuzzle.option_images.length; | |
| updateOptionImage(); | |
| }); | |
| rightBtn.addEventListener('click', () => { | |
| currentIndex = (currentIndex + 1) % currentPuzzle.option_images.length; | |
| updateOptionImage(); | |
| }); | |
| function updateOptionImage() { | |
| // Update the option image | |
| optionImg.src = currentPuzzle.option_images[currentIndex]; | |
| // Update the indicator dots | |
| const dots = indicatorContainer.querySelectorAll('.dot'); | |
| dots.forEach((dot, i) => { | |
| dot.className = i === currentIndex ? 'dot active' : 'dot'; | |
| }); | |
| // Update the current index in the puzzle data | |
| currentPuzzle.current_option_index = currentIndex; | |
| } | |
| // Auto-show submit button for image matching puzzles | |
| const submitSection = document.createElement('div'); | |
| submitSection.className = 'image-matching-submit'; | |
| const matchingSubmitBtn = document.createElement('button'); | |
| matchingSubmitBtn.textContent = 'Submit'; | |
| matchingSubmitBtn.className = 'submit-image-matching'; | |
| matchingSubmitBtn.addEventListener('click', submitAnswer); | |
| submitSection.appendChild(matchingSubmitBtn); | |
| // Add to puzzle container | |
| imageWrapper.appendChild(submitSection); | |
| } | |
| // Function to set up patch selection grid | |
| function setupPatchSelectGrid() { | |
| // Remove any existing grid first | |
| const existingGrid = document.querySelector('.patch-select-grid'); | |
| if (existingGrid) { | |
| existingGrid.remove(); | |
| } | |
| // Clear the puzzle image container | |
| puzzleImageContainer.innerHTML = ''; | |
| // IMPORTANT: Reset the global selectedCells array to fix the bug | |
| // when encountering these puzzles multiple times | |
| selectedCells = []; | |
| // Create a container for the patch select grid | |
| const gridContainer = document.createElement('div'); | |
| gridContainer.className = 'patch-select-grid'; | |
| // Get grid dimensions from the puzzle data | |
| const gridSize = currentPuzzle.grid_size || [6, 6]; | |
| const rows = gridSize[0]; | |
| const cols = gridSize[1]; | |
| // Set grid styles | |
| gridContainer.style.display = 'grid'; | |
| gridContainer.style.gridTemplateColumns = `repeat(${cols}, 1fr)`; | |
| gridContainer.style.gridTemplateRows = `repeat(${rows}, 1fr)`; | |
| gridContainer.style.gap = '3px'; | |
| gridContainer.style.width = '100%'; | |
| gridContainer.style.aspectRatio = `${cols}/${rows}`; | |
| gridContainer.style.position = 'relative'; | |
| // Create image container | |
| const imageContainer = document.createElement('div'); | |
| imageContainer.className = 'patch-select-image-container'; | |
| imageContainer.style.position = 'absolute'; | |
| imageContainer.style.top = '0'; | |
| imageContainer.style.left = '0'; | |
| imageContainer.style.width = '100%'; | |
| imageContainer.style.height = '100%'; | |
| imageContainer.style.zIndex = '0'; | |
| // Add the puzzle image | |
| const img = document.createElement('img'); | |
| img.src = currentPuzzle.image_path; | |
| img.alt = 'CAPTCHA image'; | |
| img.style.width = '100%'; | |
| img.style.height = '100%'; | |
| img.style.objectFit = 'cover'; | |
| imageContainer.appendChild(img); | |
| // Add image container to grid container | |
| gridContainer.appendChild(imageContainer); | |
| // Create grid cells for selection | |
| // Use the global selectedCells array directly | |
| for (let i = 0; i < rows * cols; i++) { | |
| const cell = document.createElement('div'); | |
| cell.className = 'patch-select-cell'; | |
| cell.dataset.index = i; | |
| cell.style.position = 'relative'; | |
| cell.style.zIndex = '1'; | |
| cell.style.cursor = 'pointer'; | |
| // Add a checkmark icon to indicate selection | |
| const checkmark = document.createElement('div'); | |
| checkmark.className = 'checkmark'; | |
| checkmark.innerHTML = '✓'; | |
| checkmark.style.position = 'absolute'; | |
| checkmark.style.top = '50%'; | |
| checkmark.style.left = '50%'; | |
| checkmark.style.transform = 'translate(-50%, -50%)'; | |
| checkmark.style.color = 'white'; | |
| checkmark.style.fontSize = '32px'; | |
| checkmark.style.fontWeight = 'bold'; | |
| checkmark.style.opacity = '0'; | |
| checkmark.style.transition = 'opacity 0.2s ease'; | |
| checkmark.style.pointerEvents = 'none'; | |
| checkmark.style.textShadow = '1px 1px 3px rgba(0, 0, 0, 0.7)'; | |
| checkmark.style.zIndex = '3'; | |
| cell.appendChild(checkmark); | |
| // Add click event to toggle selection | |
| cell.addEventListener('click', () => { | |
| // Toggle selection | |
| if (cell.classList.contains('selected')) { | |
| cell.classList.remove('selected'); | |
| // Hide checkmark | |
| checkmark.style.opacity = '0'; | |
| // Remove from selected array | |
| const index = selectedCells.indexOf(i); | |
| if (index > -1) { | |
| selectedCells.splice(index, 1); | |
| } | |
| } else { | |
| cell.classList.add('selected'); | |
| // Show checkmark | |
| checkmark.style.opacity = '1'; | |
| // Add to selected array | |
| selectedCells.push(i); | |
| } | |
| // Update the answer in the UI | |
| userAnswerInput.value = JSON.stringify(selectedCells); | |
| // Enable the submit button when squares are selected | |
| submitBtn.disabled = false; | |
| // Log selected patches for debugging | |
| console.log('Selected patches:', selectedCells); | |
| }); | |
| gridContainer.appendChild(cell); | |
| } | |
| // Add the grid to the puzzle container | |
| puzzleImageContainer.appendChild(gridContainer); | |
| // Update the prompt to include the target object | |
| puzzlePrompt.textContent = `Select all squares with ${currentPuzzle.target_object}`; | |
| // Hide the regular input and replace with verify button | |
| userAnswerInput.style.display = 'none'; | |
| submitBtn.textContent = 'Verify'; | |
| submitBtn.style.display = 'inline-block'; // Changed to inline-block | |
| inputGroup.style.display = 'flex'; | |
| submitBtn.disabled = false; // Ensure the button is enabled | |
| // Clear any previous answer | |
| userAnswerInput.value = '[]'; | |
| } | |
| // Function to set up Select_Animal grid | |
| function setupSelectAnimalGrid() { | |
| // Remove any existing grid first | |
| const existingGrid = document.querySelector('.animal-select-grid'); | |
| if (existingGrid) { | |
| existingGrid.remove(); | |
| } | |
| // Clear the puzzle image container | |
| puzzleImageContainer.innerHTML = ''; | |
| // IMPORTANT: Reset the selectedAnimalIndex to -1 to fix the bug when encountering this puzzle multiple times | |
| selectedAnimalIndex = -1; | |
| // Create a simple container directly | |
| const container = document.createElement('div'); | |
| container.style.width = '100%'; | |
| container.style.maxWidth = '800px'; | |
| container.style.margin = '0 auto'; | |
| container.style.position = 'relative'; | |
| // Display the image directly | |
| const img = document.createElement('img'); | |
| img.src = currentPuzzle.image_path; | |
| img.alt = 'CAPTCHA image with animals'; | |
| img.style.width = '100%'; | |
| img.style.display = 'block'; | |
| img.style.border = '2px solid #ccc'; | |
| img.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.15)'; | |
| container.appendChild(img); | |
| // Get grid dimensions from the puzzle data | |
| const gridSize = currentPuzzle.grid_size || [2, 3]; | |
| const rows = gridSize[0]; | |
| const cols = gridSize[1]; | |
| // Wait for image to load to ensure dimensions are available | |
| img.onload = function() { | |
| // Create overlay grid that matches the image dimensions | |
| const grid = document.createElement('div'); | |
| grid.style.position = 'absolute'; | |
| grid.style.top = '0'; | |
| grid.style.left = '0'; | |
| grid.style.width = '100%'; | |
| grid.style.height = '100%'; | |
| grid.style.display = 'grid'; | |
| grid.style.gridTemplateColumns = `repeat(${cols}, 1fr)`; | |
| grid.style.gridTemplateRows = `repeat(${rows}, 1fr)`; | |
| // IMPORTANT: Create a fresh selectedAnimal object with -1 index to fix the bug | |
| // when encountering these puzzles multiple times | |
| const selectedAnimal = { index: -1 }; | |
| for (let i = 0; i < rows * cols; i++) { | |
| const cell = document.createElement('div'); | |
| cell.style.border = '1px solid rgba(255, 255, 255, 0.3)'; | |
| cell.style.cursor = 'pointer'; | |
| cell.style.position = 'relative'; | |
| cell.style.transition = 'all 0.2s ease'; | |
| // Add hover effect | |
| cell.addEventListener('mouseover', () => { | |
| cell.style.backgroundColor = 'rgba(76, 175, 80, 0.2)'; | |
| cell.style.border = '1px solid rgba(76, 175, 80, 0.7)'; | |
| }); | |
| cell.addEventListener('mouseout', () => { | |
| if (selectedAnimal.index !== i) { | |
| cell.style.backgroundColor = 'transparent'; | |
| cell.style.border = '1px solid rgba(255, 255, 255, 0.3)'; | |
| } | |
| }); | |
| // Add click event to toggle selection | |
| cell.addEventListener('click', () => { | |
| // Clear previous selection | |
| grid.querySelectorAll('div').forEach((c, index) => { | |
| if (index !== i) { | |
| c.style.backgroundColor = 'transparent'; | |
| c.style.border = '1px solid rgba(255, 255, 255, 0.3)'; | |
| } | |
| }); | |
| // Update selection | |
| selectedAnimal.index = i; | |
| selectedAnimalIndex = i; // Update the global variable | |
| cell.style.backgroundColor = 'rgba(76, 175, 80, 0.3)'; | |
| cell.style.border = '2px solid rgba(76, 175, 80, 0.9)'; | |
| // Update the answer in the UI | |
| userAnswerInput.value = JSON.stringify([i]); | |
| // Enable the submit button | |
| submitBtn.disabled = false; | |
| // Log selected animal for debugging | |
| console.log('Selected animal at index:', i); | |
| }); | |
| grid.appendChild(cell); | |
| } | |
| // Add the grid to the container | |
| container.appendChild(grid); | |
| }; | |
| // Add the container to the puzzle container | |
| puzzleImageContainer.appendChild(container); | |
| // Make sure the prompt is clearly visible | |
| puzzlePrompt.style.fontSize = '20px'; | |
| puzzlePrompt.style.fontWeight = 'bold'; | |
| puzzlePrompt.style.marginBottom = '20px'; | |
| // Update the prompt to include the target animal | |
| puzzlePrompt.textContent = `Pick a ${currentPuzzle.target_object}`; | |
| // Hide the regular input and replace with verify button | |
| userAnswerInput.style.display = 'none'; | |
| submitBtn.textContent = 'Submit'; | |
| submitBtn.style.display = 'inline-block'; | |
| inputGroup.style.display = 'flex'; | |
| submitBtn.disabled = true; // Disabled until selection is made | |
| // Clear any previous answer | |
| userAnswerInput.value = '[]'; | |
| } | |
| /** | |
| * Setup the Object Match interface with reference image and option controls | |
| */ | |
| function setupObjectMatch() { | |
| // Create container for the object match interface | |
| const matchContainer = document.createElement('div'); | |
| matchContainer.className = 'object-match-container'; | |
| // Create a horizontal layout | |
| const horizontalLayout = document.createElement('div'); | |
| horizontalLayout.className = 'object-match-horizontal-layout'; | |
| // Create reference image container | |
| const referenceContainer = document.createElement('div'); | |
| referenceContainer.className = 'object-match-reference'; | |
| // Add reference image | |
| const referenceImage = document.createElement('img'); | |
| referenceImage.src = currentPuzzle.reference_image || currentPuzzle.additional_data.reference_image; | |
| referenceImage.alt = 'Reference Image'; | |
| referenceImage.className = 'object-match-reference-img'; | |
| referenceContainer.appendChild(referenceImage); | |
| // Add reference caption | |
| const referenceCaption = document.createElement('div'); | |
| referenceCaption.className = 'object-match-caption'; | |
| referenceCaption.textContent = 'Match This!'; | |
| referenceContainer.appendChild(referenceCaption); | |
| // Create options container | |
| const optionsContainer = document.createElement('div'); | |
| optionsContainer.className = 'object-match-options'; | |
| // Add option image | |
| const optionImage = document.createElement('img'); | |
| const optionImages = currentPuzzle.option_images || currentPuzzle.additional_data.option_images; | |
| optionImage.src = optionImages[0]; // Start with first option | |
| optionImage.alt = 'Option Image'; | |
| optionImage.className = 'object-match-option-img'; | |
| optionsContainer.appendChild(optionImage); | |
| // Create navigation controls | |
| const controlsContainer = document.createElement('div'); | |
| controlsContainer.className = 'object-match-controls'; | |
| // Left arrow | |
| const leftArrow = document.createElement('button'); | |
| leftArrow.innerHTML = '←'; | |
| leftArrow.className = 'object-match-arrow left-arrow'; | |
| leftArrow.addEventListener('click', () => updateObjectOption(-1)); | |
| // Right arrow | |
| const rightArrow = document.createElement('button'); | |
| rightArrow.innerHTML = '→'; | |
| rightArrow.className = 'object-match-arrow right-arrow'; | |
| rightArrow.addEventListener('click', () => updateObjectOption(1)); | |
| // Add arrows to controls | |
| controlsContainer.appendChild(leftArrow); | |
| controlsContainer.appendChild(rightArrow); | |
| // Add controls to options container | |
| optionsContainer.appendChild(controlsContainer); | |
| // Add reference and options to horizontal layout | |
| horizontalLayout.appendChild(referenceContainer); | |
| horizontalLayout.appendChild(optionsContainer); | |
| // Add horizontal layout to main container | |
| matchContainer.appendChild(horizontalLayout); | |
| // Add option indicators (dots) | |
| const indicators = document.createElement('div'); | |
| indicators.className = 'object-match-indicators'; | |
| const numOptions = optionImages.length; | |
| for (let i = 0; i < numOptions; i++) { | |
| const dot = document.createElement('span'); | |
| dot.className = 'object-match-dot'; | |
| if (i === 0) { | |
| dot.classList.add('active'); | |
| } | |
| indicators.appendChild(dot); | |
| } | |
| // Add indicators to main container | |
| matchContainer.appendChild(indicators); | |
| // Add submit button | |
| const submitBtn = document.createElement('button'); | |
| submitBtn.textContent = 'Submit'; | |
| submitBtn.className = 'object-match-submit'; | |
| submitBtn.addEventListener('click', submitAnswer); | |
| // Add containers to puzzle image container | |
| puzzleImageContainer.appendChild(matchContainer); | |
| puzzleImageContainer.appendChild(submitBtn); | |
| // Store current index in data attribute | |
| puzzleImageContainer.dataset.currentOptionIndex = '0'; | |
| // Log for debugging | |
| console.log('Object Match images:', { | |
| reference: referenceImage.src, | |
| options: optionImages | |
| }); | |
| } | |
| /** | |
| * Update the displayed option image based on navigation direction | |
| * @param {number} direction - Direction to navigate (-1 for left, 1 for right) | |
| */ | |
| function updateObjectOption(direction) { | |
| const container = document.querySelector('.object-match-container'); | |
| const optionImage = document.querySelector('.object-match-option-img'); | |
| const dots = document.querySelectorAll('.object-match-dot'); | |
| // Get current index | |
| let currentIndex = parseInt(puzzleImageContainer.dataset.currentOptionIndex); | |
| const optionImages = currentPuzzle.option_images || currentPuzzle.additional_data.option_images; | |
| const numOptions = optionImages.length; | |
| // Calculate new index with wrap-around | |
| let newIndex = (currentIndex + direction + numOptions) % numOptions; | |
| // Update the option image | |
| optionImage.src = optionImages[newIndex]; | |
| // Update dots | |
| dots.forEach((dot, index) => { | |
| if (index === newIndex) { | |
| dot.classList.add('active'); | |
| } else { | |
| dot.classList.remove('active'); | |
| } | |
| }); | |
| // Store new index | |
| puzzleImageContainer.dataset.currentOptionIndex = newIndex.toString(); | |
| // Store selected answer for submission | |
| userAnswerInput.value = newIndex.toString(); | |
| // Log for debugging | |
| console.log('Updated option image:', { | |
| index: newIndex, | |
| src: optionImage.src | |
| }); | |
| } | |
| /** | |
| * Setup the Place_Dot interface allowing the user to click on the image to place a dot | |
| */ | |
| function setupPlaceDot() { | |
| // Clear the puzzle image container | |
| puzzleImageContainer.innerHTML = ''; | |
| // Create a container for the image with relative positioning | |
| const container = document.createElement('div'); | |
| container.style.position = 'relative'; | |
| container.style.width = '100%'; | |
| container.style.maxWidth = '800px'; | |
| container.style.margin = '0 auto'; | |
| // Create and add the image | |
| const img = document.createElement('img'); | |
| img.src = `/captcha_data/${currentPuzzle.puzzle_type}/${currentPuzzle.puzzle_id}`; | |
| img.alt = 'Car path image'; | |
| img.style.width = '100%'; | |
| img.style.display = 'block'; | |
| img.style.cursor = 'crosshair'; | |
| container.appendChild(img); | |
| // Reset any previous click coordinates | |
| clickCoordinates = null; | |
| // Add click handler to the image | |
| img.addEventListener('click', (e) => { | |
| // Remove any existing dot | |
| const existingDot = container.querySelector('.place-dot-marker'); | |
| if (existingDot) { | |
| existingDot.remove(); | |
| } | |
| // Get click coordinates relative to the image | |
| const rect = e.target.getBoundingClientRect(); | |
| const x = Math.round(e.clientX - rect.left); | |
| const y = Math.round(e.clientY - rect.top); | |
| // Store coordinates for submission | |
| clickCoordinates = [x, y]; | |
| // Create dot marker | |
| const dot = document.createElement('div'); | |
| dot.className = 'place-dot-marker'; | |
| dot.style.position = 'absolute'; | |
| dot.style.width = '20px'; | |
| dot.style.height = '20px'; | |
| dot.style.borderRadius = '50%'; | |
| dot.style.backgroundColor = 'rgba(255, 0, 0, 0.7)'; | |
| dot.style.border = '2px solid #ff0000'; | |
| dot.style.left = `${x}px`; | |
| dot.style.top = `${y}px`; | |
| dot.style.transform = 'translate(-50%, -50%)'; | |
| dot.style.pointerEvents = 'none'; | |
| dot.style.zIndex = '10'; | |
| // Add animation | |
| dot.style.animation = 'pulse 1s infinite alternate'; | |
| // Add dot to container | |
| container.appendChild(dot); | |
| // Enable submit button | |
| submitBtn.disabled = false; | |
| // Log coordinates for debugging | |
| console.log('Dot placed at:', { x, y }); | |
| }); | |
| // Add the container to the puzzle container | |
| puzzleImageContainer.appendChild(container); | |
| // In debug mode, fetch the ground truth to show the target area | |
| if (DEBUG_MODE) { | |
| fetch('/api/get_ground_truth', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify({ | |
| puzzle_type: currentPuzzle.puzzle_type, | |
| puzzle_id: currentPuzzle.puzzle_id | |
| }) | |
| }) | |
| .then(response => response.json()) | |
| .then(gtData => { | |
| // Check if we have a target position in the answer | |
| if (gtData.answer && gtData.answer.target_position) { | |
| const targetPosition = gtData.answer.target_position; | |
| const tolerance = gtData.answer.tolerance || 15; // Default to 15px | |
| showTargetDotArea(container, targetPosition, tolerance); | |
| } | |
| }) | |
| .catch(error => { | |
| console.error('Error fetching ground truth for Place_Dot:', error); | |
| }); | |
| } | |
| // Update prompt and input elements | |
| puzzlePrompt.textContent = currentPuzzle.prompt || "Click to place a Dot at the end of the car's path"; | |
| // Hide the input field and adjust the submit button | |
| userAnswerInput.style.display = 'none'; | |
| submitBtn.textContent = 'Submit'; | |
| submitBtn.disabled = true; // Disabled until user places a dot | |
| submitBtn.style.display = 'inline-block'; | |
| inputGroup.style.display = 'flex'; | |
| } | |
| /** | |
| * Show the target area for the Place_Dot puzzle in debug mode | |
| * @param {HTMLElement} container - The container element | |
| * @param {Array} targetPosition - The target position [x, y] | |
| * @param {number} tolerance - The tolerance radius in pixels | |
| */ | |
| function showTargetDotArea(container, targetPosition, tolerance = 15) { | |
| if (!DEBUG_MODE) return; | |
| // Remove any existing target visualization | |
| const existingTarget = container.querySelector('.target-dot-area'); | |
| if (existingTarget) { | |
| existingTarget.remove(); | |
| } | |
| // Get target coordinates | |
| const [targetX, targetY] = targetPosition; | |
| // Create a target element - visualized as a circle | |
| const targetArea = document.createElement('div'); | |
| targetArea.className = 'target-dot-area'; | |
| // Calculate diameter based on tolerance | |
| const diameter = tolerance * 2; | |
| // Style the target area | |
| targetArea.style.position = 'absolute'; | |
| targetArea.style.left = `${targetX - tolerance}px`; | |
| targetArea.style.top = `${targetY - tolerance}px`; | |
| targetArea.style.width = `${diameter}px`; | |
| targetArea.style.height = `${diameter}px`; | |
| targetArea.style.borderRadius = '50%'; | |
| targetArea.style.border = '2px dashed green'; | |
| targetArea.style.backgroundColor = 'rgba(0, 255, 0, 0.2)'; | |
| targetArea.style.zIndex = '5'; | |
| targetArea.style.pointerEvents = 'none'; // Allow clicks to pass through | |
| // Add coordinates label | |
| const coordsLabel = document.createElement('div'); | |
| coordsLabel.className = 'coords-label'; | |
| coordsLabel.textContent = `Target: (${targetX}, ${targetY}) ±${tolerance}px`; | |
| coordsLabel.style.position = 'absolute'; | |
| coordsLabel.style.top = '-25px'; | |
| coordsLabel.style.left = '0'; | |
| coordsLabel.style.backgroundColor = 'rgba(0, 0, 0, 0.7)'; | |
| coordsLabel.style.color = 'white'; | |
| coordsLabel.style.padding = '2px 5px'; | |
| coordsLabel.style.fontSize = '10px'; | |
| coordsLabel.style.borderRadius = '3px'; | |
| coordsLabel.style.whiteSpace = 'nowrap'; | |
| targetArea.appendChild(coordsLabel); | |
| // Add to the container | |
| container.appendChild(targetArea); | |
| // Log the target details | |
| console.log('Place_Dot target position:', { | |
| x: targetX, | |
| y: targetY, | |
| tolerance: tolerance | |
| }); | |
| } | |
| // Function to set up connect icon interface | |
| function setupConnectIcon() { | |
| // Clear the puzzle image container | |
| puzzleImageContainer.innerHTML = ''; | |
| // Create a layout container for the two-column layout | |
| const layoutContainer = document.createElement('div'); | |
| layoutContainer.className = 'connect-icon-layout'; | |
| layoutContainer.style.display = 'flex'; | |
| layoutContainer.style.justifyContent = 'space-between'; | |
| // Create container for reference image | |
| const refContainer = document.createElement('div'); | |
| refContainer.className = 'reference-image-container'; | |
| refContainer.style.flex = '1'; | |
| refContainer.style.marginRight = '10px'; | |
| refContainer.style.textAlign = 'center'; | |
| // Add "Match This!" label above reference image | |
| const matchLabel = document.createElement('div'); | |
| matchLabel.className = 'match-label'; | |
| matchLabel.textContent = 'Match This!'; | |
| matchLabel.style.backgroundColor = 'black'; | |
| matchLabel.style.color = 'white'; | |
| matchLabel.style.padding = '2px 5px'; | |
| matchLabel.style.marginBottom = '5px'; | |
| matchLabel.style.fontSize = '12px'; | |
| refContainer.appendChild(matchLabel); | |
| // Add reference image | |
| const refImg = document.createElement('img'); | |
| refImg.id = 'connect-reference-image'; | |
| refImg.src = currentPuzzle.reference_image; | |
| refImg.alt = 'Reference image'; | |
| refImg.style.maxWidth = '100%'; | |
| refImg.style.border = '1px solid #ccc'; | |
| refContainer.appendChild(refImg); | |
| // Container for option images with arrows | |
| const optionContainer = document.createElement('div'); | |
| optionContainer.className = 'connect-option-container'; | |
| optionContainer.style.flex = '1'; | |
| optionContainer.style.position = 'relative'; | |
| // Create option image display | |
| const optionImgContainer = document.createElement('div'); | |
| optionImgContainer.className = 'option-image-container'; | |
| optionImgContainer.style.textAlign = 'center'; | |
| // Create option image | |
| const optionImg = document.createElement('img'); | |
| optionImg.id = 'connect-option-image'; | |
| optionImg.src = currentPuzzle.option_images[0]; // Start with the first option | |
| optionImg.alt = 'Option image'; | |
| optionImg.style.maxWidth = '100%'; | |
| optionImg.style.border = '1px solid #ccc'; | |
| optionImgContainer.appendChild(optionImg); | |
| optionContainer.appendChild(optionImgContainer); | |
| // Add arrow navigation | |
| const arrowsContainer = document.createElement('div'); | |
| arrowsContainer.className = 'connect-arrows-container'; | |
| arrowsContainer.style.display = 'flex'; | |
| arrowsContainer.style.justifyContent = 'center'; | |
| arrowsContainer.style.marginTop = '10px'; | |
| // Left arrow | |
| const leftArrow = document.createElement('button'); | |
| leftArrow.className = 'arrow-btn left-arrow'; | |
| leftArrow.innerHTML = '←'; // Left arrow character | |
| leftArrow.setAttribute('aria-label', 'Previous option'); | |
| leftArrow.style.margin = '0 10px'; | |
| leftArrow.style.padding = '5px 15px'; | |
| leftArrow.style.fontSize = '20px'; | |
| leftArrow.style.backgroundColor = '#f0f0f0'; | |
| leftArrow.style.border = '1px solid #ccc'; | |
| leftArrow.style.borderRadius = '4px'; | |
| leftArrow.style.cursor = 'pointer'; | |
| // Right arrow | |
| const rightArrow = document.createElement('button'); | |
| rightArrow.className = 'arrow-btn right-arrow'; | |
| rightArrow.innerHTML = '→'; // Right arrow character | |
| rightArrow.setAttribute('aria-label', 'Next option'); | |
| rightArrow.style.margin = '0 10px'; | |
| rightArrow.style.padding = '5px 15px'; | |
| rightArrow.style.fontSize = '20px'; | |
| rightArrow.style.backgroundColor = '#f0f0f0'; | |
| rightArrow.style.border = '1px solid #ccc'; | |
| rightArrow.style.borderRadius = '4px'; | |
| rightArrow.style.cursor = 'pointer'; | |
| arrowsContainer.appendChild(leftArrow); | |
| arrowsContainer.appendChild(rightArrow); | |
| optionContainer.appendChild(arrowsContainer); | |
| // Add pagination dots | |
| const dotsContainer = document.createElement('div'); | |
| dotsContainer.className = 'pagination-dots'; | |
| dotsContainer.style.display = 'flex'; | |
| dotsContainer.style.justifyContent = 'center'; | |
| dotsContainer.style.marginTop = '10px'; | |
| // Create dots based on the number of options | |
| for (let i = 0; i < currentPuzzle.option_images.length; i++) { | |
| const dot = document.createElement('span'); | |
| dot.className = 'pagination-dot'; | |
| dot.style.height = '10px'; | |
| dot.style.width = '10px'; | |
| dot.style.margin = '0 5px'; | |
| dot.style.borderRadius = '50%'; | |
| dot.style.backgroundColor = i === 0 ? '#4CAF50' : '#ccc'; // Highlight first dot | |
| dotsContainer.appendChild(dot); | |
| } | |
| optionContainer.appendChild(dotsContainer); | |
| // Add all containers to the layout | |
| layoutContainer.appendChild(refContainer); | |
| layoutContainer.appendChild(optionContainer); | |
| puzzleImageContainer.appendChild(layoutContainer); | |
| // Add a submit button | |
| const submitSection = document.createElement('div'); | |
| submitSection.className = 'connect-icon-submit'; | |
| submitSection.style.textAlign = 'center'; | |
| submitSection.style.marginTop = '15px'; | |
| const submitBtn = document.createElement('button'); | |
| submitBtn.textContent = 'Submit'; | |
| submitBtn.className = 'submit-connect'; | |
| submitBtn.style.padding = '10px 20px'; | |
| submitBtn.style.backgroundColor = '#4CAF50'; | |
| submitBtn.style.color = 'white'; | |
| submitBtn.style.border = 'none'; | |
| submitBtn.style.borderRadius = '4px'; | |
| submitBtn.style.fontSize = '16px'; | |
| submitBtn.style.cursor = 'pointer'; | |
| submitBtn.addEventListener('click', submitAnswer); | |
| submitSection.appendChild(submitBtn); | |
| // Add to puzzle container | |
| puzzleImageContainer.appendChild(submitSection); | |
| // Set up current option tracking | |
| let currentOptionIndex = 0; | |
| // Initialize the answer input with the current index | |
| userAnswerInput.value = currentOptionIndex.toString(); | |
| // Function to update the option image | |
| function updateConnectOptionImage() { | |
| const optionImg = document.getElementById('connect-option-image'); | |
| if (optionImg) { | |
| optionImg.src = currentPuzzle.option_images[currentOptionIndex]; | |
| } | |
| // Update dots to highlight current option | |
| const dots = document.querySelectorAll('.pagination-dot'); | |
| dots.forEach((dot, index) => { | |
| dot.style.backgroundColor = index === currentOptionIndex ? '#4CAF50' : '#ccc'; | |
| }); | |
| // Update the answer input with the current index | |
| userAnswerInput.value = currentOptionIndex.toString(); | |
| } | |
| // Event listeners for arrows | |
| leftArrow.addEventListener('click', () => { | |
| currentOptionIndex = (currentOptionIndex - 1 + currentPuzzle.option_images.length) % currentPuzzle.option_images.length; | |
| updateConnectOptionImage(); | |
| }); | |
| rightArrow.addEventListener('click', () => { | |
| currentOptionIndex = (currentOptionIndex + 1) % currentPuzzle.option_images.length; | |
| updateConnectOptionImage(); | |
| }); | |
| } | |
| // Function to set up Click Order interface | |
| function setupClickOrder() { | |
| // Clear the puzzle image container | |
| puzzleImageContainer.innerHTML = ''; | |
| // Create a container for the layout | |
| const layoutContainer = document.createElement('div'); | |
| layoutContainer.className = 'click-order-layout'; | |
| layoutContainer.style.display = 'flex'; | |
| layoutContainer.style.flexDirection = 'column'; | |
| layoutContainer.style.alignItems = 'center'; | |
| // Create a container for the main image | |
| const mainImageContainer = document.createElement('div'); | |
| mainImageContainer.className = 'main-image-container'; | |
| mainImageContainer.style.position = 'relative'; | |
| mainImageContainer.style.marginBottom = '20px'; | |
| mainImageContainer.style.width = '100%'; | |
| // Add main image | |
| const mainImg = document.createElement('img'); | |
| mainImg.id = 'click-order-main-image'; | |
| mainImg.src = currentPuzzle.image_path; | |
| mainImg.alt = 'Click the icons in order'; | |
| mainImg.style.maxWidth = '100%'; | |
| mainImg.style.border = '1px solid #ccc'; | |
| mainImageContainer.appendChild(mainImg); | |
| // Create a container for the order reference image | |
| const orderImageContainer = document.createElement('div'); | |
| orderImageContainer.className = 'order-image-container'; | |
| orderImageContainer.style.textAlign = 'center'; | |
| orderImageContainer.style.marginBottom = '20px'; | |
| // Add "Order Reference" label | |
| const orderLabel = document.createElement('div'); | |
| orderLabel.className = 'order-label'; | |
| orderLabel.textContent = 'Click icons in this order:'; | |
| orderLabel.style.backgroundColor = 'black'; | |
| orderLabel.style.color = 'white'; | |
| orderLabel.style.padding = '5px'; | |
| orderLabel.style.marginBottom = '5px'; | |
| orderLabel.style.fontSize = '14px'; | |
| orderImageContainer.appendChild(orderLabel); | |
| // Add order reference image | |
| const orderImg = document.createElement('img'); | |
| orderImg.id = 'click-order-reference-image'; | |
| orderImg.src = currentPuzzle.order_image; | |
| orderImg.alt = 'Reference order'; | |
| orderImg.style.maxWidth = '100%'; | |
| orderImg.style.border = '1px solid #ccc'; | |
| orderImageContainer.appendChild(orderImg); | |
| // Add click markers container to show user clicks | |
| const markersContainer = document.createElement('div'); | |
| markersContainer.className = 'click-markers-container'; | |
| markersContainer.style.position = 'absolute'; | |
| markersContainer.style.top = '0'; | |
| markersContainer.style.left = '0'; | |
| markersContainer.style.width = '100%'; | |
| markersContainer.style.height = '100%'; | |
| markersContainer.style.pointerEvents = 'none'; // Don't block clicks | |
| mainImageContainer.appendChild(markersContainer); | |
| // Track user clicks | |
| let userClicks = []; | |
| // Add click indicator | |
| const clickIndicator = document.createElement('div'); | |
| clickIndicator.className = 'click-indicator'; | |
| clickIndicator.style.marginTop = '10px'; | |
| clickIndicator.style.fontSize = '16px'; | |
| clickIndicator.textContent = 'Clicks: 0'; | |
| // Add reset button | |
| const resetButton = document.createElement('button'); | |
| resetButton.textContent = 'Reset Clicks'; | |
| resetButton.className = 'reset-clicks-btn'; | |
| resetButton.style.padding = '8px 15px'; | |
| resetButton.style.backgroundColor = '#f44336'; | |
| resetButton.style.color = 'white'; | |
| resetButton.style.border = 'none'; | |
| resetButton.style.borderRadius = '4px'; | |
| resetButton.style.marginRight = '10px'; | |
| resetButton.style.cursor = 'pointer'; | |
| // Add click event handler for the main image | |
| mainImg.addEventListener('click', function(e) { | |
| // Get click coordinates relative to the image | |
| const rect = e.target.getBoundingClientRect(); | |
| const x = Math.round(e.clientX - rect.left); | |
| const y = Math.round(e.clientY - rect.top); | |
| // Add click to the array | |
| userClicks.push([x, y]); | |
| // Show click marker | |
| addClickMarker(x, y, userClicks.length, markersContainer); | |
| // Update click indicator | |
| clickIndicator.textContent = `Clicks: ${userClicks.length}`; | |
| // Enable the dedicated submit button if at least one click has been made | |
| clickOrderSubmitBtn.disabled = false; | |
| // Log for debugging | |
| console.log(`Click ${userClicks.length} at:`, { x, y }); | |
| }); | |
| // Event listener for reset button | |
| resetButton.addEventListener('click', function() { | |
| // Clear user clicks | |
| userClicks = []; | |
| // Clear markers | |
| markersContainer.innerHTML = ''; | |
| // Update click indicator | |
| clickIndicator.textContent = 'Clicks: 0'; | |
| // Disable submit button | |
| submitBtn.disabled = true; | |
| }); | |
| // Add components to layout | |
| layoutContainer.appendChild(orderImageContainer); | |
| layoutContainer.appendChild(mainImageContainer); | |
| // Add controls container | |
| const controlsContainer = document.createElement('div'); | |
| controlsContainer.className = 'controls-container'; | |
| controlsContainer.style.display = 'flex'; | |
| controlsContainer.style.justifyContent = 'center'; | |
| controlsContainer.style.alignItems = 'center'; | |
| controlsContainer.style.marginTop = '15px'; | |
| // Add controls to container | |
| controlsContainer.appendChild(resetButton); | |
| controlsContainer.appendChild(clickIndicator); | |
| // Add controls to layout | |
| layoutContainer.appendChild(controlsContainer); | |
| // Create a dedicated submit button for the Click Order puzzle | |
| const clickOrderSubmitBtn = document.createElement('button'); | |
| clickOrderSubmitBtn.textContent = 'Submit Order'; | |
| clickOrderSubmitBtn.className = 'click-order-submit-btn'; | |
| clickOrderSubmitBtn.style.padding = '10px 20px'; | |
| clickOrderSubmitBtn.style.backgroundColor = '#4CAF50'; | |
| clickOrderSubmitBtn.style.color = 'white'; | |
| clickOrderSubmitBtn.style.border = 'none'; | |
| clickOrderSubmitBtn.style.borderRadius = '4px'; | |
| clickOrderSubmitBtn.style.marginTop = '15px'; | |
| clickOrderSubmitBtn.style.cursor = 'pointer'; | |
| clickOrderSubmitBtn.style.fontSize = '16px'; | |
| clickOrderSubmitBtn.disabled = true; // Disabled until clicks are made | |
| // Add submit button to layout | |
| layoutContainer.appendChild(clickOrderSubmitBtn); | |
| // Add layout to puzzle container | |
| puzzleImageContainer.appendChild(layoutContainer); | |
| // Hide the original input field and submit button | |
| userAnswerInput.style.display = 'none'; | |
| submitBtn.style.display = 'none'; | |
| inputGroup.style.display = 'none'; | |
| // Enable the dedicated submit button when clicks are made | |
| mainImg.addEventListener('click', function() { | |
| if (userClicks.length > 0) { | |
| clickOrderSubmitBtn.disabled = false; | |
| } | |
| }); | |
| // Reset button should disable submit button | |
| resetButton.addEventListener('click', function() { | |
| clickOrderSubmitBtn.disabled = true; | |
| }); | |
| // Add event listener to the dedicated submit button | |
| clickOrderSubmitBtn.addEventListener('click', function() { | |
| // Set the clicks as the answer | |
| userAnswerInput.value = JSON.stringify(userClicks); | |
| // Send the data to the server | |
| fetch('/api/check_answer', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify({ | |
| puzzle_type: currentPuzzle.puzzle_type, | |
| puzzle_id: currentPuzzle.puzzle_id, | |
| answer: userClicks | |
| }) | |
| }) | |
| .then(response => response.json()) | |
| .then(data => { | |
| // Update stats | |
| benchmarkStats.total++; | |
| if (data.correct) { | |
| benchmarkStats.correct++; | |
| resultMessage.textContent = 'Correct!'; | |
| resultMessage.className = 'result-message correct'; | |
| } else { | |
| resultMessage.textContent = 'Incorrect.'; | |
| resultMessage.className = 'result-message incorrect'; | |
| } | |
| updateStats(); | |
| // Record benchmark result | |
| recordBenchmarkResult({ | |
| puzzle_type: currentPuzzle.puzzle_type, | |
| puzzle_id: currentPuzzle.puzzle_id, | |
| user_answer: userClicks, | |
| correct_answer: data.correct_answer, | |
| correct: data.correct | |
| }); | |
| // Disable the submit button | |
| clickOrderSubmitBtn.disabled = true; | |
| // Load a new puzzle after a delay | |
| setTimeout(loadNewPuzzle, 2000); | |
| }) | |
| .catch(error => { | |
| console.error('Error checking answer:', error); | |
| resultMessage.textContent = 'Error checking answer. Please try again.'; | |
| resultMessage.className = 'result-message incorrect'; | |
| // Re-enable the submit button on error | |
| clickOrderSubmitBtn.disabled = false; | |
| }); | |
| }); | |
| // In debug mode, show the correct click positions | |
| if (DEBUG_MODE) { | |
| showClickOrderAnswerPositions(mainImageContainer); | |
| } | |
| } | |
| // Function to add a numbered click marker | |
| function addClickMarker(x, y, number, container) { | |
| const marker = document.createElement('div'); | |
| marker.className = 'click-marker'; | |
| marker.style.position = 'absolute'; | |
| marker.style.left = `${x - 15}px`; | |
| marker.style.top = `${y - 15}px`; | |
| marker.style.width = '30px'; | |
| marker.style.height = '30px'; | |
| marker.style.borderRadius = '50%'; | |
| marker.style.backgroundColor = 'rgba(255, 0, 0, 0.6)'; | |
| marker.style.border = '2px solid white'; | |
| marker.style.color = 'white'; | |
| marker.style.fontWeight = 'bold'; | |
| marker.style.display = 'flex'; | |
| marker.style.justifyContent = 'center'; | |
| marker.style.alignItems = 'center'; | |
| marker.style.fontSize = '14px'; | |
| marker.style.zIndex = '100'; | |
| marker.style.pointerEvents = 'none'; // Don't block future clicks | |
| marker.textContent = number.toString(); | |
| container.appendChild(marker); | |
| } | |
| // Function to show correct answer positions in debug mode | |
| function showClickOrderAnswerPositions(container) { | |
| fetch('/api/get_ground_truth', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify({ | |
| puzzle_type: currentPuzzle.puzzle_type, | |
| puzzle_id: currentPuzzle.puzzle_id | |
| }) | |
| }) | |
| .then(response => response.json()) | |
| .then(gtData => { | |
| if (gtData.answer && Array.isArray(gtData.answer)) { | |
| const correctPositions = gtData.answer; | |
| const tolerance = currentPuzzle.tolerance || 20; | |
| // Create a debug layer | |
| const debugLayer = document.createElement('div'); | |
| debugLayer.className = 'debug-layer'; | |
| debugLayer.style.position = 'absolute'; | |
| debugLayer.style.top = '0'; | |
| debugLayer.style.left = '0'; | |
| debugLayer.style.width = '100%'; | |
| debugLayer.style.height = '100%'; | |
| debugLayer.style.pointerEvents = 'none'; | |
| // Add correct position indicators | |
| correctPositions.forEach((pos, index) => { | |
| const [x, y] = pos; | |
| // Create circle for tolerance area | |
| const toleranceCircle = document.createElement('div'); | |
| toleranceCircle.className = 'tolerance-circle'; | |
| toleranceCircle.style.position = 'absolute'; | |
| toleranceCircle.style.left = `${x - tolerance}px`; | |
| toleranceCircle.style.top = `${y - tolerance}px`; | |
| toleranceCircle.style.width = `${tolerance * 2}px`; | |
| toleranceCircle.style.height = `${tolerance * 2}px`; | |
| toleranceCircle.style.borderRadius = '50%'; | |
| toleranceCircle.style.border = '2px dashed green'; | |
| toleranceCircle.style.backgroundColor = 'rgba(0, 255, 0, 0.1)'; | |
| // Create label with position number | |
| const posLabel = document.createElement('div'); | |
| posLabel.className = 'position-label'; | |
| posLabel.style.position = 'absolute'; | |
| posLabel.style.left = `${x}px`; | |
| posLabel.style.top = `${y - 20}px`; | |
| posLabel.style.transform = 'translate(-50%, -50%)'; | |
| posLabel.style.backgroundColor = 'rgba(0, 0, 0, 0.7)'; | |
| posLabel.style.color = 'white'; | |
| posLabel.style.padding = '2px 5px'; | |
| posLabel.style.borderRadius = '3px'; | |
| posLabel.style.fontSize = '10px'; | |
| posLabel.textContent = `${index + 1}: (${x}, ${y})`; | |
| debugLayer.appendChild(toleranceCircle); | |
| debugLayer.appendChild(posLabel); | |
| }); | |
| container.appendChild(debugLayer); | |
| } | |
| }) | |
| .catch(error => { | |
| console.error('Error fetching ground truth for Click_Order:', error); | |
| }); | |
| } | |
| // Function to setup the Hold Button CAPTCHA | |
| function setupHoldButton() { | |
| // record the start time | |
| puzzleStartTime = Date.now(); | |
| // Clear the puzzle image container first | |
| puzzleImageContainer.innerHTML = ''; | |
| // Create a container for the button | |
| const buttonContainer = document.createElement('div'); | |
| buttonContainer.className = 'hold-button-container'; | |
| buttonContainer.style.position = 'relative'; | |
| buttonContainer.style.width = '100%'; | |
| buttonContainer.style.maxWidth = '400px'; | |
| buttonContainer.style.margin = '0 auto'; | |
| buttonContainer.style.textAlign = 'center'; | |
| // If the CAPTCHA has an image, show it above the button | |
| if (currentPuzzle.image_path) { | |
| const imageElement = document.createElement('img'); | |
| imageElement.src = currentPuzzle.image_path; | |
| imageElement.alt = 'Hold Button CAPTCHA'; | |
| imageElement.style.display = 'block'; | |
| imageElement.style.width = '100%'; | |
| imageElement.style.maxWidth = '400px'; | |
| imageElement.style.margin = '0 auto 20px'; | |
| imageElement.style.borderRadius = '8px'; | |
| buttonContainer.appendChild(imageElement); | |
| } | |
| // Create button element | |
| const button = document.createElement('div'); | |
| button.className = 'hold-button'; | |
| button.style.position = 'relative'; | |
| button.style.width = '100%'; | |
| button.style.height = 'auto'; | |
| button.style.cursor = 'pointer'; | |
| button.style.userSelect = 'none'; | |
| button.style.borderRadius = '50px'; | |
| button.style.border = '3px solid #333'; | |
| button.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.2)'; | |
| button.style.backgroundColor = '#f8f8f8'; | |
| button.style.padding = '30px 0'; | |
| button.style.fontSize = '28px'; | |
| button.style.fontWeight = 'bold'; | |
| button.style.color = '#333'; | |
| button.style.textAlign = 'center'; | |
| button.style.transition = 'background-color 0.3s'; | |
| button.textContent = 'HOLD'; | |
| // Create progress bar | |
| const progressBar = document.createElement('div'); | |
| progressBar.className = 'hold-progress'; | |
| progressBar.style.position = 'absolute'; | |
| progressBar.style.left = '0'; | |
| progressBar.style.bottom = '0'; | |
| progressBar.style.height = '8px'; | |
| progressBar.style.width = '0%'; | |
| progressBar.style.backgroundColor = '#4CAF50'; | |
| progressBar.style.transition = 'width 0.1s linear'; | |
| progressBar.style.borderRadius = '0 0 50px 50px'; | |
| // Get hold time from data | |
| const requiredHoldTime = currentPuzzle.hold_time || 3; // Default to 3 seconds | |
| // Variables to track holding | |
| let isHolding = false; | |
| let holdStartTime = 0; | |
| let holdTimer = null; | |
| let completed = false; | |
| let currentHoldTime = 0; | |
| // Add event listeners for hold detection | |
| button.addEventListener('mousedown', startHolding); | |
| button.addEventListener('touchstart', startHolding); | |
| document.addEventListener('mouseup', stopHolding); | |
| document.addEventListener('touchend', stopHolding); | |
| function startHolding(e) { | |
| if (completed) return; | |
| // Prevent default behaviors for touch | |
| if (e.type === 'touchstart') { | |
| e.preventDefault(); | |
| } | |
| isHolding = true; | |
| holdStartTime = Date.now(); | |
| button.style.backgroundColor = '#e0e0e0'; | |
| // Start progress animation | |
| holdTimer = setInterval(() => { | |
| if (!isHolding) return; | |
| const elapsedTime = (Date.now() - holdStartTime) / 1000; // in seconds | |
| currentHoldTime = elapsedTime; | |
| // Update progress bar | |
| const progress = Math.min((elapsedTime / requiredHoldTime) * 100, 100); | |
| progressBar.style.width = `${progress}%`; | |
| // Check if hold is complete | |
| if (elapsedTime >= requiredHoldTime && !completed) { | |
| completeHold(); | |
| } | |
| }, 100); // Update every 100ms | |
| } | |
| function stopHolding() { | |
| if (!isHolding || completed) return; | |
| isHolding = false; | |
| button.style.backgroundColor = '#f8f8f8'; | |
| // Reset progress if not completed | |
| if (!completed) { | |
| progressBar.style.width = '0%'; | |
| clearInterval(holdTimer); | |
| } | |
| } | |
| function completeHold() { | |
| completed = true; | |
| clearInterval(holdTimer); | |
| // Change button appearance | |
| button.style.backgroundColor = '#4CAF50'; | |
| button.style.color = 'white'; | |
| button.textContent = 'COMPLETED'; | |
| // Set the user answer to the current hold time | |
| userAnswerInput.value = currentHoldTime.toFixed(2); | |
| // Enable submit button | |
| submitBtn.disabled = false; | |
| resultMessage.textContent = "Button hold completed! Click 'Submit' to continue."; | |
| resultMessage.className = 'result-message instruction'; | |
| } | |
| // Add the progress bar to button | |
| button.appendChild(progressBar); | |
| // Add button to container | |
| buttonContainer.appendChild(button); | |
| // Add to puzzle container | |
| puzzleImageContainer.appendChild(buttonContainer); | |
| // Reset and clear input field | |
| userAnswerInput.value = ''; | |
| userAnswerInput.style.display = 'none'; | |
| submitBtn.disabled = false; // Disable submit button until hold is complete | |
| } | |
| // Function to show dotted areas in debug mode for Pick_Area | |
| function showPickAreaTargets(container) { | |
| if (!DEBUG_MODE || !currentPuzzle) return; | |
| // Fetch ground truth data to show the correct area | |
| fetch('/api/get_ground_truth', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify({ | |
| puzzle_type: currentPuzzle.puzzle_type, | |
| puzzle_id: currentPuzzle.puzzle_id | |
| }) | |
| }) | |
| .then(response => response.json()) | |
| .then(gtData => { | |
| if (gtData.answer && gtData.answer.area) { | |
| // Get the area from ground truth | |
| const areaCoords = gtData.answer.area; | |
| const areaType = gtData.answer.type || 'largest region'; | |
| // Create a marker for the area | |
| const areaMarker = document.createElement('div'); | |
| areaMarker.className = 'area-marker debug-marker'; | |
| areaMarker.style.position = 'absolute'; | |
| areaMarker.style.border = '3px dashed #ff3333'; | |
| // Use a more transparent background to show the underlying dotted lines | |
| areaMarker.style.backgroundColor = 'rgba(255, 51, 51, 0.15)'; | |
| areaMarker.style.zIndex = '999'; | |
| // Add border radius to better represent curved areas | |
| areaMarker.style.borderRadius = '25%'; | |
| // Set position and size | |
| const [topLeft, bottomRight] = areaCoords; | |
| const [minX, minY] = topLeft; | |
| const [maxX, maxY] = bottomRight; | |
| areaMarker.style.left = `${minX}px`; | |
| areaMarker.style.top = `${minY}px`; | |
| areaMarker.style.width = `${maxX - minX}px`; | |
| areaMarker.style.height = `${maxY - minY}px`; | |
| // Add a label that better explains what to do | |
| const label = document.createElement('div'); | |
| label.className = 'debug-label'; | |
| label.style.position = 'absolute'; | |
| label.style.top = '5px'; | |
| label.style.left = '50%'; | |
| label.style.transform = 'translateX(-50%)'; | |
| label.style.backgroundColor = 'rgba(0, 0, 0, 0.7)'; | |
| label.style.color = 'white'; | |
| label.style.padding = '5px 10px'; | |
| label.style.fontSize = '14px'; | |
| label.style.fontWeight = 'bold'; | |
| label.style.borderRadius = '3px'; | |
| label.style.whiteSpace = 'nowrap'; | |
| label.style.textAlign = 'center'; | |
| label.textContent = `${areaType}: (${minX},${minY}) to (${maxX},${maxY})`; | |
| areaMarker.appendChild(label); | |
| // Add a note to explain that the actual area follows the dotted lines | |
| const note = document.createElement('div'); | |
| note.className = 'area-note'; | |
| note.style.position = 'absolute'; | |
| note.style.bottom = '10px'; | |
| note.style.left = '50%'; | |
| note.style.transform = 'translateX(-50%)'; | |
| note.style.backgroundColor = 'rgba(0, 0, 0, 0.7)'; | |
| note.style.color = 'white'; | |
| note.style.padding = '5px 10px'; | |
| note.style.fontSize = '12px'; | |
| note.style.borderRadius = '3px'; | |
| note.style.maxWidth = '90%'; | |
| note.style.textAlign = 'center'; | |
| note.textContent = 'Follow the dotted white lines to identify the actual area'; | |
| areaMarker.appendChild(note); | |
| container.appendChild(areaMarker); | |
| // Create indicators to highlight the dotted lines | |
| // This is a simplistic approach; ideally we would trace the actual dotted lines | |
| highlightDottedLines(container, areaCoords); | |
| } | |
| }) | |
| .catch(error => { | |
| console.error('Error fetching ground truth for Pick_Area:', error); | |
| }); | |
| } | |
| // Function to highlight the dotted lines that define the area | |
| function highlightDottedLines(container, areaCoords) { | |
| const [topLeft, bottomRight] = areaCoords; | |
| const [minX, minY] = topLeft; | |
| const [maxX, maxY] = bottomRight; | |
| // Create a canvas element to draw over the image | |
| const canvas = document.createElement('canvas'); | |
| canvas.className = 'dotted-line-highlight'; | |
| canvas.style.position = 'absolute'; | |
| canvas.style.top = '0'; | |
| canvas.style.left = '0'; | |
| canvas.style.pointerEvents = 'none'; // Don't interfere with clicks | |
| canvas.style.zIndex = '998'; // Just below the area marker | |
| // Wait for the image to load to get the correct dimensions | |
| const img = container.querySelector('img'); | |
| if (!img) return; | |
| if (img.complete) { | |
| setupCanvas(); | |
| } else { | |
| img.onload = setupCanvas; | |
| } | |
| function setupCanvas() { | |
| canvas.width = img.clientWidth; | |
| canvas.height = img.clientHeight; | |
| const ctx = canvas.getContext('2d'); | |
| ctx.strokeStyle = 'rgba(255, 255, 255, 0.7)'; | |
| ctx.lineWidth = 3; | |
| ctx.setLineDash([5, 5]); // Create a dashed line effect | |
| // Draw a path that approximates the dotted lines | |
| // This is just a rough approximation - would need image processing to trace actual lines | |
| ctx.beginPath(); | |
| // Top line | |
| ctx.moveTo(minX, minY); | |
| ctx.lineTo(maxX, minY); | |
| // Right line | |
| ctx.moveTo(maxX, minY); | |
| ctx.lineTo(maxX, maxY); | |
| // Bottom line | |
| ctx.moveTo(maxX, maxY); | |
| ctx.lineTo(minX, maxY); | |
| // Left line | |
| ctx.moveTo(minX, maxY); | |
| ctx.lineTo(minX, minY); | |
| ctx.stroke(); | |
| container.appendChild(canvas); | |
| } | |
| } | |
| // Function to show the area to avoid for Misleading_Click puzzles | |
| function showMisleadingClickArea(container, avoidArea) { | |
| if (!DEBUG_MODE || !avoidArea) return; | |
| // Create a marker for the area to avoid | |
| const areaMarker = document.createElement('div'); | |
| areaMarker.className = 'avoid-area-marker debug-marker'; | |
| areaMarker.style.position = 'absolute'; | |
| areaMarker.style.border = '3px dashed red'; | |
| areaMarker.style.backgroundColor = 'rgba(255, 0, 0, 0.3)'; | |
| areaMarker.style.zIndex = '999'; | |
| // Set position and size | |
| const { x, y, width, height } = avoidArea; | |
| areaMarker.style.left = `${x}px`; | |
| areaMarker.style.top = `${y}px`; | |
| areaMarker.style.width = `${width}px`; | |
| areaMarker.style.height = `${height}px`; | |
| // Add a label | |
| const label = document.createElement('div'); | |
| label.className = 'debug-label'; | |
| label.style.position = 'absolute'; | |
| label.style.top = '-20px'; | |
| label.style.left = '0'; | |
| label.style.backgroundColor = 'rgba(0, 0, 0, 0.7)'; | |
| label.style.color = 'white'; | |
| label.style.padding = '2px 5px'; | |
| label.style.fontSize = '12px'; | |
| label.style.borderRadius = '3px'; | |
| label.textContent = `DO NOT CLICK IN THIS AREA: (${x},${y}) ${width}x${height}`; | |
| // Add a warning sign in the middle | |
| const warningSign = document.createElement('div'); | |
| warningSign.className = 'warning-sign'; | |
| warningSign.textContent = 'DO NOT CLICK HERE'; | |
| warningSign.style.position = 'absolute'; | |
| warningSign.style.top = '50%'; | |
| warningSign.style.left = '50%'; | |
| warningSign.style.transform = 'translate(-50%, -50%)'; | |
| warningSign.style.backgroundColor = 'rgba(0, 0, 0, 0.7)'; | |
| warningSign.style.color = '#ff5555'; | |
| warningSign.style.padding = '5px 10px'; | |
| warningSign.style.fontSize = '14px'; | |
| warningSign.style.fontWeight = 'bold'; | |
| warningSign.style.borderRadius = '3px'; | |
| warningSign.style.whiteSpace = 'nowrap'; | |
| warningSign.style.zIndex = '10'; | |
| areaMarker.appendChild(label); | |
| areaMarker.appendChild(warningSign); | |
| container.appendChild(areaMarker); | |
| console.log('Misleading Click area to avoid:', avoidArea); | |
| } | |
| /** | |
| * Checks if a point is inside a polygon defined by an array of points | |
| * Uses ray-casting algorithm | |
| * @param {number} x - X coordinate of the point to check | |
| * @param {number} y - Y coordinate of the point to check | |
| * @param {array} polygon - Array of points defining the polygon [[x1,y1], [x2,y2], ...] | |
| * @returns {boolean} True if the point is inside the polygon | |
| */ | |
| function pointInPolygon(x, y, polygon) { | |
| if (!polygon || polygon.length < 3) return false; | |
| let inside = false; | |
| for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { | |
| const xi = polygon[i][0], yi = polygon[i][1]; | |
| const xj = polygon[j][0], yj = polygon[j][1]; | |
| const intersect = ((yi > y) != (yj > y)) && | |
| (x < (xj - xi) * (y - yi) / (yj - yi) + xi); | |
| if (intersect) inside = !inside; | |
| } | |
| return inside; | |
| } | |
| /** | |
| * Sets up the Dart Count interface with reference number and dart images | |
| */ | |
| function setupDartCount() { | |
| // Clear the puzzle image container | |
| puzzleImageContainer.innerHTML = ''; | |
| // Create container for the dart count interface | |
| const dartContainer = document.createElement('div'); | |
| dartContainer.className = 'dart-count-container'; | |
| // Create a horizontal layout | |
| const horizontalLayout = document.createElement('div'); | |
| horizontalLayout.className = 'dart-count-horizontal-layout'; | |
| // Create reference container (shows the target number) | |
| const referenceContainer = document.createElement('div'); | |
| referenceContainer.className = 'dart-count-reference'; | |
| // Add reference image - check all possible locations for data | |
| const referenceImage = document.createElement('img'); | |
| if (currentPuzzle.additional_data && currentPuzzle.additional_data.reference_image) { | |
| referenceImage.src = currentPuzzle.additional_data.reference_image; | |
| } else if (currentPuzzle.reference_image) { | |
| referenceImage.src = currentPuzzle.reference_image; | |
| } else { | |
| console.error('Reference image not found for Dart Count puzzle'); | |
| } | |
| referenceImage.alt = 'Target Number'; | |
| referenceImage.className = 'dart-count-reference-img'; | |
| referenceContainer.appendChild(referenceImage); | |
| // Add reference caption | |
| const referenceCaption = document.createElement('div'); | |
| referenceCaption.className = 'dart-count-caption'; | |
| referenceCaption.textContent = 'Find sum of darts equal to this'; | |
| referenceContainer.appendChild(referenceCaption); | |
| // Create options container | |
| const optionsContainer = document.createElement('div'); | |
| optionsContainer.className = 'dart-count-options'; | |
| // Get option images from all possible locations | |
| let optionImages = []; | |
| if (currentPuzzle.additional_data && currentPuzzle.additional_data.option_images) { | |
| optionImages = currentPuzzle.additional_data.option_images; | |
| } else if (currentPuzzle.option_images) { | |
| optionImages = currentPuzzle.option_images; | |
| } else { | |
| console.error('Option images not found for Dart Count puzzle'); | |
| optionImages = []; | |
| } | |
| // Add option image | |
| const optionImage = document.createElement('img'); | |
| if (optionImages.length > 0) { | |
| optionImage.src = optionImages[0]; // Start with first option | |
| } | |
| optionImage.alt = 'Dart Option'; | |
| optionImage.className = 'dart-count-option-img'; | |
| optionsContainer.appendChild(optionImage); | |
| // Create navigation controls | |
| const controlsContainer = document.createElement('div'); | |
| controlsContainer.className = 'dart-count-controls'; | |
| // Left arrow | |
| const leftArrow = document.createElement('button'); | |
| leftArrow.innerHTML = '←'; | |
| leftArrow.className = 'dart-count-arrow left-arrow'; | |
| leftArrow.addEventListener('click', () => updateDartOption(-1)); | |
| // Right arrow | |
| const rightArrow = document.createElement('button'); | |
| rightArrow.innerHTML = '→'; | |
| rightArrow.className = 'dart-count-arrow right-arrow'; | |
| rightArrow.addEventListener('click', () => updateDartOption(1)); | |
| // Add arrows to controls | |
| controlsContainer.appendChild(leftArrow); | |
| controlsContainer.appendChild(rightArrow); | |
| // Add controls to options container | |
| optionsContainer.appendChild(controlsContainer); | |
| // Add reference and options to horizontal layout | |
| horizontalLayout.appendChild(referenceContainer); | |
| horizontalLayout.appendChild(optionsContainer); | |
| // Add horizontal layout to main container | |
| dartContainer.appendChild(horizontalLayout); | |
| // Add option indicators (dots) | |
| const indicators = document.createElement('div'); | |
| indicators.className = 'dart-count-indicators'; | |
| const numOptions = optionImages.length; | |
| for (let i = 0; i < numOptions; i++) { | |
| const dot = document.createElement('span'); | |
| dot.className = 'dart-count-dot'; | |
| if (i === 0) { | |
| dot.classList.add('active'); | |
| } | |
| indicators.appendChild(dot); | |
| } | |
| // Add indicators to main container | |
| dartContainer.appendChild(indicators); | |
| // Add submit button | |
| const submitBtn = document.createElement('button'); | |
| submitBtn.textContent = 'Submit'; | |
| submitBtn.className = 'dart-count-submit'; | |
| submitBtn.addEventListener('click', submitAnswer); | |
| // Add containers to puzzle image container | |
| puzzleImageContainer.appendChild(dartContainer); | |
| puzzleImageContainer.appendChild(submitBtn); | |
| // Store current index in the hidden input for submission | |
| userAnswerInput.value = '0'; | |
| // Log all available data for debugging | |
| console.log('Dart Count puzzle data:', currentPuzzle); | |
| } | |
| /** | |
| * Update the displayed dart option image based on navigation direction | |
| * @param {number} direction - Direction to navigate (-1 for left, 1 for right) | |
| */ | |
| function updateDartOption(direction) { | |
| const optionImage = document.querySelector('.dart-count-option-img'); | |
| const dots = document.querySelectorAll('.dart-count-dot'); | |
| // Get option images from all possible locations | |
| let optionImages = []; | |
| if (currentPuzzle.additional_data && currentPuzzle.additional_data.option_images) { | |
| optionImages = currentPuzzle.additional_data.option_images; | |
| } else if (currentPuzzle.option_images) { | |
| optionImages = currentPuzzle.option_images; | |
| } else { | |
| console.error('Option images not found for Dart Count puzzle'); | |
| return; | |
| } | |
| // Get current index from input field | |
| let currentIndex = parseInt(userAnswerInput.value) || 0; | |
| const numOptions = optionImages.length; | |
| // Calculate new index with wrap-around | |
| let newIndex = (currentIndex + direction + numOptions) % numOptions; | |
| // Update the option image | |
| optionImage.src = optionImages[newIndex]; | |
| // Update dots | |
| dots.forEach((dot, index) => { | |
| if (index === newIndex) { | |
| dot.classList.add('active'); | |
| } else { | |
| dot.classList.remove('active'); | |
| } | |
| }); | |
| // Store selected answer for submission | |
| userAnswerInput.value = newIndex.toString(); | |
| // Log for debugging | |
| console.log('Updated dart option:', { | |
| index: newIndex, | |
| src: optionImage.src | |
| }); | |
| } | |
| /** | |
| * Display difficulty stars based on CAPTCHA type | |
| * @param {string} puzzleType - The type of CAPTCHA puzzle | |
| */ | |
| function displayDifficultyStars(puzzleType) { | |
| const difficultyRatings = { | |
| 'Dice_Count': 1, | |
| 'Geometry_Click': 1, | |
| 'Rotation_Match': 3, | |
| 'Slide_Puzzle': 2, | |
| 'Unusual_Detection': 3, | |
| 'Image_Recognition': 2, | |
| 'Bingo': 4, | |
| 'Image_Matching': 2, | |
| 'Patch_Select': 3, | |
| 'Dart_Count': 3, | |
| 'Object_Match': 2, | |
| 'Select_Animal': 1, | |
| 'Coordinates': 3, | |
| 'Path_Finder': 3, | |
| 'Place_Dot': 2, | |
| 'Connect_icon': 3, | |
| 'Click_Order': 4, | |
| 'Hold_Button': 2, | |
| 'Misleading_Click': 4, | |
| 'Pick_Area': 5, | |
| }; | |
| const difficulty = difficultyRatings[puzzleType] || 1; | |
| const starsContainer = document.getElementById('difficulty-stars'); | |
| // Safety check to ensure the container exists | |
| if (!starsContainer) { | |
| console.error('Stars container not found!'); | |
| return; | |
| } | |
| // Clear the container | |
| starsContainer.innerHTML = ''; | |
| // Create and append stars | |
| for (let i = 0; i < 5; i++) { | |
| const star = document.createElement('span'); | |
| star.className = 'star'; | |
| star.innerHTML = i < difficulty ? '★' : '☆'; // Filled or empty star | |
| starsContainer.appendChild(star); | |
| } | |
| // Log for debugging | |
| console.log(`Displayed ${difficulty} stars for puzzle type: ${puzzleType}`); | |
| } | |
| // Function to get a new puzzle | |
| function getPuzzle(callback) { | |
| let queryParams = ''; | |
| // Check if debug mode is active and add the debug_type parameter if it is | |
| if (DEBUG_MODE && DEBUG_TYPE) { | |
| queryParams = `?debug_type=${encodeURIComponent(DEBUG_TYPE)}`; | |
| } | |
| fetch('/api/get_puzzle' + queryParams) | |
| .then(response => response.json()) | |
| .then(data => { | |
| currentPuzzle = data; | |
| // Log the data for debugging | |
| console.log('Puzzle data:', data); | |
| // Set the prompt and update debug information | |
| const promptElement = document.getElementById('puzzle-prompt'); | |
| promptElement.textContent = data.prompt; | |
| // Display difficulty stars based on puzzle type | |
| displayDifficultyStars(data.puzzle_type); | |
| // Update debug indicator if in debug mode | |
| const debugIndicator = document.getElementById('debug-indicator'); | |
| const debugTypeDisplay = document.getElementById('debug-type-display'); | |
| if (DEBUG_MODE && DEBUG_TYPE) { | |
| debugIndicator.style.display = 'block'; | |
| debugTypeDisplay.textContent = DEBUG_TYPE; | |
| } else { | |
| debugIndicator.style.display = 'none'; | |
| } | |
| // Handle different input types | |
| const imageContainer = document.getElementById('puzzle-image-container'); | |
| const userAnswerInput = document.getElementById('user-answer'); | |
| const submitBtn = document.getElementById('submit-answer'); | |
| // Reset the input field and enable submit button | |
| userAnswerInput.value = ''; | |
| submitBtn.disabled = false; | |
| // Clear any previous result message | |
| const resultMessage = document.getElementById('result-message'); | |
| resultMessage.textContent = ''; | |
| resultMessage.className = 'result-message'; | |
| // Clear the puzzle image container | |
| imageContainer.innerHTML = ''; | |
| // Set up UI based on input type | |
| if (data.input_type === 'number') { | |
| // For numeric input (e.g., Dice_Count) | |
| userAnswerInput.type = 'number'; | |
| userAnswerInput.placeholder = 'Enter number'; | |
| userAnswerInput.style.display = 'block'; | |
| submitBtn.style.display = 'block'; | |
| // Load the image | |
| const img = document.createElement('img'); | |
| img.src = data.image_path; | |
| img.alt = 'CAPTCHA Puzzle'; | |
| img.id = 'puzzle-image'; | |
| img.onload = function() { | |
| imageContainer.appendChild(img); | |
| }; | |
| } else if (data.input_type === 'click') { | |
| // For click-based puzzles (Geometry_Click, Place_Dot, etc.) | |
| userAnswerInput.style.display = 'none'; | |
| submitBtn.style.display = 'none'; | |
| // Load the image and set up click handler | |
| const img = document.createElement('img'); | |
| img.src = data.image_path; | |
| img.alt = 'CAPTCHA Puzzle'; | |
| img.id = 'puzzle-image'; | |
| img.onclick = handleImageClick; | |
| img.onload = function() { | |
| imageContainer.appendChild(img); | |
| // For Misleading_Click, show the area to avoid in debug mode | |
| if (data.puzzle_type === 'Misleading_Click' && DEBUG_MODE) { | |
| showMisleadingClickArea(imageContainer, data.avoid_area); | |
| } | |
| // For Pick_Area, show the target areas in debug mode | |
| if (data.puzzle_type === 'Pick_Area' && DEBUG_MODE) { | |
| showPickAreaTargets(imageContainer); | |
| } | |
| }; | |
| } else if (data.input_type === 'rotation') { | |
| // For rotation puzzles | |
| userAnswerInput.style.display = 'none'; | |
| submitBtn.style.display = 'block'; | |
| // Set up rotation controls | |
| setupRotationControls(); | |
| } else if (data.input_type === 'slide') { | |
| // For slide puzzles | |
| userAnswerInput.style.display = 'none'; | |
| submitBtn.style.display = 'block'; | |
| // Set up slide puzzle | |
| setupSlidePuzzle(); | |
| } else if (data.input_type === 'multiselect') { | |
| // For multiple selection puzzles (Unusual_Detection) | |
| userAnswerInput.style.display = 'none'; | |
| submitBtn.style.display = 'block'; | |
| // Set up grid for unusual detection | |
| setupUnusualDetectionGrid(); | |
| } else if (data.input_type === 'bingo_swap') { | |
| // For bingo swap puzzles | |
| userAnswerInput.style.display = 'none'; | |
| submitBtn.style.display = 'block'; | |
| // Set up bingo swap interface | |
| setupBingoSwap(); | |
| } else if (data.input_type === 'image_grid') { | |
| // For image grid puzzles (Image_Recognition) | |
| userAnswerInput.style.display = 'none'; | |
| submitBtn.style.display = 'block'; | |
| // Set up image recognition grid | |
| setupImageRecognition(); | |
| } else if (data.input_type === 'image_matching') { | |
| // For image matching puzzles | |
| userAnswerInput.style.display = 'none'; | |
| submitBtn.style.display = 'block'; | |
| // Set up image matching interface | |
| setupImageMatching(); | |
| } else if (data.input_type === 'patch_select') { | |
| // For patch select puzzles | |
| userAnswerInput.style.display = 'none'; | |
| submitBtn.style.display = 'block'; | |
| // Set up patch select grid | |
| setupPatchSelectGrid(); | |
| } else if (data.input_type === 'dart_count') { | |
| // For dart count puzzles | |
| userAnswerInput.style.display = 'none'; | |
| submitBtn.style.display = 'none'; | |
| // Set up dart count interface | |
| setupDartCount(); | |
| } else if (data.input_type === 'object_match') { | |
| // For object match puzzles | |
| userAnswerInput.style.display = 'none'; | |
| submitBtn.style.display = 'block'; | |
| // Set up object match interface | |
| setupObjectMatch(); | |
| } else if (data.input_type === 'select_animal') { | |
| // For animal selection puzzles | |
| userAnswerInput.style.display = 'none'; | |
| submitBtn.style.display = 'block'; | |
| // Set up animal selection grid | |
| setupSelectAnimalGrid(); | |
| } else if (data.input_type === 'place_dot') { | |
| // For place dot puzzles | |
| userAnswerInput.style.display = 'none'; | |
| submitBtn.style.display = 'none'; | |
| // Set up place dot interface | |
| setupPlaceDot(); | |
| } else if (data.input_type === 'connect_icon') { | |
| // For connect icon puzzles | |
| userAnswerInput.style.display = 'none'; | |
| submitBtn.style.display = 'block'; | |
| // Set up connect icon interface | |
| setupConnectIcon(); | |
| } else if (data.input_type === 'click_order') { | |
| // For click order puzzles | |
| userAnswerInput.style.display = 'none'; | |
| submitBtn.style.display = 'block'; | |
| // Set up click order interface | |
| setupClickOrder(); | |
| } else if (data.input_type === 'hold_button') { | |
| // For hold button puzzles | |
| userAnswerInput.style.display = 'none'; | |
| submitBtn.style.display = 'block'; | |
| // Set up hold button interface | |
| setupHoldButton(); | |
| } else { | |
| // Default to text input for other types | |
| userAnswerInput.type = 'text'; | |
| userAnswerInput.placeholder = 'Your answer'; | |
| userAnswerInput.style.display = 'block'; | |
| submitBtn.style.display = 'block'; | |
| // Load the image | |
| const img = document.createElement('img'); | |
| img.src = data.image_path; | |
| img.alt = 'CAPTCHA Puzzle'; | |
| img.id = 'puzzle-image'; | |
| img.onload = function() { | |
| imageContainer.appendChild(img); | |
| }; | |
| } | |
| // Call the callback if provided | |
| if (callback && typeof callback === 'function') { | |
| callback(); | |
| } | |
| }) | |
| .catch(error => { | |
| console.error('Error fetching puzzle:', error); | |
| }); | |
| } | |
| }); | |