// ---------------------------- // Sample Email Data // ---------------------------- const emails = [ { id: 1, sender: "Interview Kicks", subject: "Can I Become an ML Engineer? Why Not, We Ask!", snippet: "It's time to banish that doubt forever. Attend our masterclass to unlock your potential.", timestamp: "1:31 PM" }, { id: 2, sender: "Career Brew", subject: "7th Sep Jobs, 87 Hottest Jobs and Early Career Jobs - Do Not Miss", snippet: "Top companies are hiring right now — don't miss your shot at these exclusive opportunities!", timestamp: "12:35 PM" }, { id: 3, sender: "Professor Smith", subject: "Research Collaboration Proposal", snippet: "I've reviewed your proposal draft. Let's schedule a call next week to discuss next steps.", timestamp: "10:22 AM" }, { id: 4, sender: "Friend", subject: "Weekend Plans", snippet: "Are we still on for the hiking trip this weekend? Let me know so I can book the gear.", timestamp: "Yesterday" }, { id: 5, sender: "Bank Support", subject: "Monthly Statement", snippet: "Your statement is ready. Minimum due: $120. Avoid penalties by paying before the 15th.", timestamp: "Sep 5" }, { id: 6, sender: "Netflix", subject: "New Releases This Week!", snippet: "Check out the hottest new shows and movies added to your watchlist this week.", timestamp: "Sep 4" }, { id: 7, sender: "Amazon", subject: "Your Order #12345 Has Shipped", snippet: "Your package is on the way! Track your delivery using the link below.", timestamp: "Sep 3" }, { id: 8, sender: "LinkedIn", subject: "You have 5 new notifications", snippet: "See who viewed your profile and new job recommendations.", timestamp: "Sep 2" }, { id: 9, sender: "Microsoft", subject: "Your Office 365 Subscription Renewal", snippet: "Your Office 365 subscription will expire in 7 days. Renew now to avoid service interruption.", timestamp: "Sep 1" }, { id: 10, sender: "TripAdvisor", subject: "Your upcoming trip to Paris", snippet: "Don't forget to check-in for your flight tomorrow. Here's your itinerary and hotel information.", timestamp: "Aug 30" } ]; // ---------------------------- // Debug Utilities // ---------------------------- class DebugManager { constructor() { this.statusEl = document.getElementById('debugStatus'); this.selectedEmailEl = document.getElementById('debugSelectedEmail'); this.gestureTypeEl = document.getElementById('debugGestureType'); this.bufferCountEl = document.getElementById('debugBufferCount'); this.circleCountEl = document.getElementById('debugCircleCount'); this.cameraStatusEl = document.getElementById('debugCameraStatus'); this.lastErrorEl = document.getElementById('debugLastError'); this.statusIndicator = document.getElementById('statusIndicator'); // Get debug toggle button this.toggleButton = document.getElementById('debugToggle'); this.debugPanel = document.getElementById('debugPanel'); // Add click event listener this.toggleButton.addEventListener('click', () => { this.toggleDebugPanel(); }); } updateStatus(status) { this.statusEl.textContent = status; } updateSelectedEmail(emailId) { if (emailId) { const email = emails.find(e => e.id === emailId); this.selectedEmailEl.textContent = email ? email.subject.substring(0, 20) + '...' : 'Unknown'; } else { this.selectedEmailEl.textContent = 'None'; } } updateGestureType(gestureType) { this.gestureTypeEl.textContent = gestureType; } updateBufferCount(count) { this.bufferCountEl.textContent = `${count} points`; } updateCircleCount(count) { this.circleCountEl.textContent = `${count} points`; } updateCameraStatus(status) { this.cameraStatusEl.textContent = status; } logError(error) { console.error("Gesture Detection Error:", error); this.lastErrorEl.textContent = error.message.substring(0, 50) + (error.message.length > 50 ? '...' : ''); // Update status indicator to red this.statusIndicator.className = 'status-indicator error'; } setReady() { this.updateStatus('Ready'); this.statusIndicator.className = 'status-indicator ready'; } setProcessing() { this.updateStatus('Processing...'); this.statusIndicator.className = 'status-indicator processing'; } toggleDebugPanel() { if (this.debugPanel.classList.contains('visible')) { this.debugPanel.classList.remove('visible'); } else { this.debugPanel.classList.add('visible'); } } } // ---------------------------- // UI Management // ---------------------------- class UIManager { constructor() { this.emailList = document.getElementById('emailList'); this.actionFeedback = document.getElementById('actionFeedback'); this.selectionHighlight = document.getElementById('selectionHighlight'); this.handLandmarks = document.getElementById('handLandmarks'); this.gesturePath = document.getElementById('gesturePath'); this.selectedEmail = null; this.emailElements = []; this.renderEmails(); this.setupEventListeners(); } renderEmails() { this.emailList.innerHTML = ''; this.emailElements = []; emails.forEach(email => { const emailElement = document.createElement('div'); emailElement.className = 'email-item'; emailElement.dataset.id = email.id; emailElement.innerHTML = `
${email.sender}
${email.timestamp}
${email.subject}
${email.snippet}
`; this.emailList.appendChild(emailElement); this.emailElements.push({ id: email.id, element: emailElement, rect: null }); }); // Update email positions this.updateEmailPositions(); } updateEmailPositions() { this.emailElements.forEach(item => { const rect = item.element.getBoundingClientRect(); item.rect = { left: rect.left, top: rect.top, right: rect.right, bottom: rect.bottom, width: rect.width, height: rect.height }; }); } selectEmail(emailId) { // Remove previous selection if (this.selectedEmail) { const prevElement = this.emailElements.find(e => e.id === this.selectedEmail); if (prevElement) { prevElement.element.classList.remove('selected'); } } // Set new selection this.selectedEmail = emailId; const newElement = this.emailElements.find(e => e.id === emailId); if (newElement) { newElement.element.classList.add('selected'); this.showSelectionHighlight(newElement.rect); } return newElement ? newElement.rect : null; } showSelectionHighlight(rect) { this.selectionHighlight.style.display = 'block'; this.selectionHighlight.style.left = `${rect.left}px`; this.selectionHighlight.style.top = `${rect.top}px`; this.selectionHighlight.style.width = `${rect.width}px`; this.selectionHighlight.style.height = `${rect.height}px`; } hideSelectionHighlight() { this.selectionHighlight.style.display = 'none'; } showActionFeedback(message, type) { this.actionFeedback.textContent = message; this.actionFeedback.className = 'action-feedback'; if (type === 'delete') { this.actionFeedback.classList.add('delete'); } else if (type === 'archive') { this.actionFeedback.classList.add('archive'); } else { this.actionFeedback.classList.add('summary'); } this.actionFeedback.classList.add('show'); setTimeout(() => { this.actionFeedback.classList.remove('show'); }, 2000); } clearSelection() { if (this.selectedEmail) { const element = this.emailElements.find(e => e.id === this.selectedEmail); if (element) { element.element.classList.remove('selected'); } this.selectedEmail = null; this.hideSelectionHighlight(); } } setupEventListeners() { window.addEventListener('resize', () => { this.updateEmailPositions(); if (this.selectedEmail) { const element = this.emailElements.find(e => e.id === this.selectedEmail); if (element) { this.showSelectionHighlight(element.rect); } } }); } // For gesture visualization updateHandLandmarks(landmarks) { this.handLandmarks.innerHTML = ''; if (!landmarks || landmarks.length === 0) return; landmarks.forEach((landmark, i) => { const landmarkEl = document.createElement('div'); landmarkEl.className = 'landmark'; if (i === 8) { // Index finger tip landmarkEl.classList.add('index-tip'); } landmarkEl.style.left = `${landmark.x * 100}%`; landmarkEl.style.top = `${landmark.y * 100}%`; this.handLandmarks.appendChild(landmarkEl); }); } updateGesturePath(points) { this.gesturePath.innerHTML = ''; if (!points || points.length === 0) return; points.forEach(point => { const pointEl = document.createElement('div'); pointEl.className = 'point'; pointEl.style.left = `${point.x * 100}%`; pointEl.style.top = `${point.y * 100}%`; this.gesturePath.appendChild(pointEl); }); } } // ---------------------------- // Gesture Detection // ---------------------------- class GestureDetector { constructor(uiManager, debugManager) { this.uiManager = uiManager; this.debugManager = debugManager; this.selectedEmailId = null; this.gestureBuffer = []; this.circlePoints = []; this.circleThreshold = 8; this.swipeThreshold = 30; this.gestureCooldown = 1500; this.lastGestureTime = 0; this.camera = null; this.debugManager.updateStatus('Setting up MediaPipe...'); this.setupMediaPipe(); } setupMediaPipe() { try { const hands = new Hands({ locateFile: (file) => { return `https://cdn.jsdelivr.net/npm/@mediapipe/hands@0.4/${file}`; } }); hands.setOptions({ maxNumHands: 1, modelComplexity: 1, minDetectionConfidence: 0.7, minTrackingConfidence: 0.7 }); hands.onResults((results) => { this.processResults(results); }); const videoElement = document.getElementById('webcam'); // Check if Camera is available if (typeof Camera === 'undefined') { this.debugManager.logError(new Error("Camera utils not loaded. Make sure camera_utils.js is included.")); return; } this.camera = new Camera(videoElement, { onFrame: async () => { try { this.debugManager.setProcessing(); await hands.send({image: videoElement}); } catch (error) { this.debugManager.logError(error); } }, width: 320, height: 240 }); this.startCamera(); } catch (error) { this.debugManager.logError(error); console.error("MediaPipe setup error:", error); } } async startCamera() { try { this.debugManager.updateCameraStatus('Starting...'); await this.camera.start(); this.debugManager.updateCameraStatus('Active'); this.debugManager.setReady(); console.log("Camera initialized successfully"); } catch (error) { this.debugManager.updateCameraStatus('Error'); this.debugManager.logError(error); // Try to get more specific error information if (error.name === 'NotAllowedError') { alert("Camera access denied. Please allow camera access in your browser settings."); } else if (error.name === 'NotFoundError') { alert("No camera found. Please connect a camera device."); } else { alert("Failed to start camera: " + error.message); } } } processResults(results) { try { // Update hand landmarks visualization if (results.multiHandLandmarks && results.multiHandLandmarks.length > 0) { this.uiManager.updateHandLandmarks(results.multiHandLandmarks[0]); // Update gesture path for debugging if (this.gestureBuffer.length > 0) { this.uiManager.updateGesturePath(this.gestureBuffer); } this.detectGesture(results.multiHandLandmarks[0]); } else { this.uiManager.clearSelection(); this.gestureBuffer = []; this.circlePoints = []; this.debugManager.updateGestureType('None'); this.debugManager.updateBufferCount(0); this.debugManager.updateCircleCount(0); } } catch (error) { this.debugManager.logError(error); } } detectGesture(landmarks) { try { const indexTip = landmarks[8]; const middleTip = landmarks[12]; // 🔥 CRITICAL FIX: Account for 60px header height! const screenX = indexTip.x * window.innerWidth; const screenY = (1 - indexTip.y) * (window.innerHeight - 60); // Subtract header height // Pointing detection (index finger higher than middle) if (indexTip.y < middleTip.y) { this.checkEmailSelection(screenX, screenY); } else { this.uiManager.clearSelection(); this.gestureBuffer = []; this.circlePoints = []; this.debugManager.updateGestureType('None'); this.debugManager.updateBufferCount(0); this.debugManager.updateCircleCount(0); } // Only process gestures if an email is selected if (this.selectedEmailId === null) { return; } // Get palm center for gesture detection const wrist = landmarks[0]; const palmCenterX = (wrist.x + landmarks[9].x) / 2; const palmCenterY = (wrist.y + landmarks[9].y) / 2; // Add to gesture buffer this.gestureBuffer.push({x: palmCenterX, y: palmCenterY}); // Check for swipe if (this.gestureBuffer.length > 2) { const prev = this.gestureBuffer[this.gestureBuffer.length - 2]; const current = this.gestureBuffer[this.gestureBuffer.length - 1]; const dx = (current.x - prev.x) * window.innerWidth; const dy = (current.y - prev.y) * window.innerHeight; // Check if it's a horizontal swipe if (Math.abs(dx) > this.swipeThreshold && Math.abs(dx) > Math.abs(dy) * 2) { if (Date.now() - this.lastGestureTime > this.gestureCooldown) { this.lastGestureTime = Date.now(); if (dx > 0) { this.debugManager.updateGestureType('Swipe Right'); this.handleGesture('swipe_right'); } else { this.debugManager.updateGestureType('Swipe Left'); this.handleGesture('swipe_left'); } this.gestureBuffer = []; this.debugManager.updateBufferCount(0); } } } // Check for circle this.circlePoints.push({x: palmCenterX, y: palmCenterY}); this.debugManager.updateCircleCount(this.circlePoints.length); if (this.circlePoints.length > this.circleThreshold) { const startPoint = this.circlePoints[0]; const endPoint = this.circlePoints[this.circlePoints.length - 1]; const dx = endPoint.x - startPoint.x; const dy = endPoint.y - startPoint.y; const distance = Math.sqrt(dx*dx + dy*dy) * window.innerWidth; // Fix: Better circle detection with radius check if (distance < 40 && Date.now() - this.lastGestureTime > this.gestureCooldown) { this.lastGestureTime = Date.now(); this.debugManager.updateGestureType('Circle'); this.handleGesture('circle'); this.circlePoints = []; this.debugManager.updateCircleCount(0); } } this.debugManager.updateBufferCount(this.gestureBuffer.length); } catch (error) { this.debugManager.logError(error); } } checkEmailSelection(x, y) { try { // Convert to page-relative coordinates (accounting for scroll) const scrollTop = window.pageYOffset || document.documentElement.scrollTop; const adjustedY = y + scrollTop; // Find the email under the finger for (let i = this.uiManager.emailElements.length - 1; i >= 0; i--) { const email = this.uiManager.emailElements[i]; if (email.rect && x >= email.rect.left && x <= email.rect.right && adjustedY >= email.rect.top && adjustedY <= email.rect.bottom) { // Only select if it's a different email if (this.selectedEmailId !== email.id) { this.selectedEmailId = email.id; this.uiManager.selectEmail(email.id); this.debugManager.updateSelectedEmail(email.id); } return; } } // If no email is selected, clear selection if (!this.uiManager.emailElements.some(email => email.rect && x >= email.rect.left && x <= email.rect.right && adjustedY >= email.rect.top && adjustedY <= email.rect.bottom)) { this.uiManager.clearSelection(); this.selectedEmailId = null; this.debugManager.updateSelectedEmail(null); } } catch (error) { this.debugManager.logError(error); } } handleGesture(gesture) { try { if (!this.selectedEmailId) return; const email = emails.find(e => e.id === this.selectedEmailId); if (!email) return; switch (gesture) { case 'swipe_left': this.uiManager.showActionFeedback(`🗑️ Deleted: ${email.subject}`, 'delete'); const index = emails.findIndex(e => e.id === this.selectedEmailId); if (index !== -1) emails.splice(index, 1); this.uiManager.renderEmails(); this.selectedEmailId = null; this.debugManager.updateSelectedEmail(null); break; case 'swipe_right': this.uiManager.showActionFeedback(`✅ Archived: ${email.subject}`, 'archive'); const archiveIndex = emails.findIndex(e => e.id === this.selectedEmailId); if (archiveIndex !== -1) emails.splice(archiveIndex, 1); this.uiManager.renderEmails(); this.selectedEmailId = null; this.debugManager.updateSelectedEmail(null); break; case 'circle': const summary = `This email discusses ${email.subject.toLowerCase()}.`; this.uiManager.showActionFeedback(`📝 Summary: ${summary}`, 'summary'); break; } } catch (error) { this.debugManager.logError(error); } } } // ---------------------------- // Initialize App // ---------------------------- document.addEventListener('DOMContentLoaded', () => { // Initialize debug manager first const debugManager = new DebugManager(); debugManager.updateStatus('Initializing UI...'); // Initialize UI const uiManager = new UIManager(); debugManager.setReady(); // Request camera access const videoElement = document.getElementById('webcam'); navigator.mediaDevices.getUserMedia({ video: true }) .then(stream => { videoElement.srcObject = stream; debugManager.updateCameraStatus('Initializing...'); // Initialize gesture detection after a short delay to ensure UI is ready setTimeout(() => { debugManager.updateStatus('Setting up gesture detection...'); try { new GestureDetector(uiManager, debugManager); } catch (error) { debugManager.logError(error); } }, 1500); }) .catch(err => { debugManager.updateCameraStatus('Denied'); debugManager.logError(err); console.error("Error accessing camera:", err); // More specific error handling if (err.name === 'NotAllowedError') { alert("Camera access denied. Please allow camera access in your browser settings."); } else if (err.name === 'NotFoundError') { alert("No camera found. Please connect a camera device."); } else { alert("Camera error: " + err.message); } }); });