|
|
|
|
|
|
|
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" |
|
} |
|
]; |
|
|
|
|
|
|
|
|
|
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'); |
|
|
|
|
|
this.toggleButton = document.getElementById('debugToggle'); |
|
this.debugPanel = document.getElementById('debugPanel'); |
|
|
|
|
|
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 ? '...' : ''); |
|
|
|
|
|
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'); |
|
} |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
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 = ` |
|
<div class="email-header"> |
|
<div class="email-sender">${email.sender}</div> |
|
<div class="email-time">${email.timestamp}</div> |
|
</div> |
|
<div class="email-subject">${email.subject}</div> |
|
<div class="email-snippet">${email.snippet}</div> |
|
`; |
|
|
|
this.emailList.appendChild(emailElement); |
|
this.emailElements.push({ |
|
id: email.id, |
|
element: emailElement, |
|
rect: null |
|
}); |
|
}); |
|
|
|
|
|
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) { |
|
|
|
if (this.selectedEmail) { |
|
const prevElement = this.emailElements.find(e => e.id === this.selectedEmail); |
|
if (prevElement) { |
|
prevElement.element.classList.remove('selected'); |
|
} |
|
} |
|
|
|
|
|
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); |
|
} |
|
} |
|
}); |
|
} |
|
|
|
|
|
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) { |
|
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); |
|
}); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
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/[email protected]/${file}`; |
|
} |
|
}); |
|
|
|
hands.setOptions({ |
|
maxNumHands: 1, |
|
modelComplexity: 1, |
|
minDetectionConfidence: 0.7, |
|
minTrackingConfidence: 0.7 |
|
}); |
|
|
|
hands.onResults((results) => { |
|
this.processResults(results); |
|
}); |
|
|
|
const videoElement = document.getElementById('webcam'); |
|
|
|
|
|
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); |
|
|
|
|
|
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 { |
|
|
|
if (results.multiHandLandmarks && results.multiHandLandmarks.length > 0) { |
|
this.uiManager.updateHandLandmarks(results.multiHandLandmarks[0]); |
|
|
|
|
|
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]; |
|
|
|
|
|
const screenX = indexTip.x * window.innerWidth; |
|
const screenY = (1 - indexTip.y) * (window.innerHeight - 60); |
|
|
|
|
|
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); |
|
} |
|
|
|
|
|
if (this.selectedEmailId === null) { |
|
return; |
|
} |
|
|
|
|
|
const wrist = landmarks[0]; |
|
const palmCenterX = (wrist.x + landmarks[9].x) / 2; |
|
const palmCenterY = (wrist.y + landmarks[9].y) / 2; |
|
|
|
|
|
this.gestureBuffer.push({x: palmCenterX, y: palmCenterY}); |
|
|
|
|
|
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; |
|
|
|
|
|
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); |
|
} |
|
} |
|
} |
|
|
|
|
|
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; |
|
|
|
|
|
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 { |
|
|
|
const scrollTop = window.pageYOffset || document.documentElement.scrollTop; |
|
const adjustedY = y + scrollTop; |
|
|
|
|
|
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) { |
|
|
|
|
|
if (this.selectedEmailId !== email.id) { |
|
this.selectedEmailId = email.id; |
|
this.uiManager.selectEmail(email.id); |
|
this.debugManager.updateSelectedEmail(email.id); |
|
} |
|
return; |
|
} |
|
} |
|
|
|
|
|
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); |
|
} |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => { |
|
|
|
const debugManager = new DebugManager(); |
|
debugManager.updateStatus('Initializing UI...'); |
|
|
|
|
|
const uiManager = new UIManager(); |
|
debugManager.setReady(); |
|
|
|
|
|
const videoElement = document.getElementById('webcam'); |
|
|
|
navigator.mediaDevices.getUserMedia({ video: true }) |
|
.then(stream => { |
|
videoElement.srcObject = stream; |
|
debugManager.updateCameraStatus('Initializing...'); |
|
|
|
|
|
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); |
|
|
|
|
|
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); |
|
} |
|
}); |
|
}); |